Skip to content

在途管理与打包发布(二) ​

本页对应 在途管理与打包发布,共 7 段视频,约 1 小时 11 分。

学习目标 ​

  1. 能够将重复的选项区域抽离为公共组件,并通过 defineProps 定义 types、dataKey 等属性,在父组件中使用属性绑定传入数组和字符串数据。
  2. 能够使用 v-for 遍历传入的选项数组,并通过点击事件同时传递选项索引和选项对象,动态判断并添加 active 类名实现选中高亮。
  3. 能够定义并导出 Pinia store,在组件中调用 useTaskStore,按选项的 dataKey 将用户选择的 id 记录到 recordData 对应字段中。
  4. 能够使用 v-model 将事故描述、故障描述、图片列表等表单数据绑定到 Pinia store 中的对应字段,并验证选择、输入和上传后数据可被正确读取。
  5. 能够根据后端接口要求将选项数据改造为包含 text 与 id 的对象结构,页面展示 text、提交时记录 id。
  6. 能够清理抽离组件后原页面中冗余的选中索引、事件处理和无效状态代码,保证组件复用后三个业务区域交互一致。

知识点 ​

本节要解决的问题 ​

在 uni-app 中使用地图服务,不能直接假设“一个地图服务商 everywhere 都能用”。因为 uni-app 要同时面对 App、H5、小程序等不同平台,而不同平台对地图 SDK 的支持范围并不一致。本节先解决“选哪家地图服务商”和“如何创建应用 Key”的问题,为后续地图能力接入做准备。

笔记给出的兼容性表如下:

地图服务商AppH5小程序
高德√√
Google仅nvue页面√
腾讯√√

从表中可以看出:H5 和小程序平台选择腾讯地图服务平台,App 选择高德地图服务平台。这样做的原因是各平台支持情况不同,不能只看“地图服务商是否知名”,而要看它在目标平台是否可用。Google 地图在 App 端仅支持 nvue 页面,普通 vue 页面不能直接按同样方式使用,因此也不作为本项目 App 端首选。

选择服务商后要创建应用 Key ​

确定服务商后,下一步不是直接写地图组件,而是先到对应开放平台注册开发者账号并创建应用。笔记要求:自行注册高德地图开发者账号并创建应用。创建应用后会得到应用 Key,后续地图能力接入需要用到它。

创建高德应用 Key 的界面示意如下:

创建高德地图应用Key界面

这里要理解两个关键字段:PackageName 和 SHA1。

PackageName 是在自定义调试基座和正式包时定义的包名。它不是应用显示名称,也不是项目文件夹名称。也就是说,调试基座使用的包名和正式包使用的包名要分别确认;如果两者不一致,Key 配置也要对应检查,不能只配一个就以为全部可用。

SHA1 是应用签名相关信息。笔记中写到“SHA1 值的获取方式如下所示”,但原笔记代码块内容为空,未给出具体命令。因此这里不能补写命令,只能保留原代码块位置,并以高德官方文档的实际获取方式为准:

bash

怎么想与怎么做 ​

整体思路可以按四步走:先看平台兼容表,确定 App 用高德、H5 和小程序用腾讯;再分别到对应开放平台注册开发者账号;然后创建应用并生成 Key;最后记录好 PackageName、SHA1 等与打包签名相关的信息,供后续配置使用。

易错点主要有:把 PackageName 当成应用名称;调试基座和正式包包名不一致却只配一个 Key;把 H5/小程序用的腾讯 Key 与 App 用的高德 Key 混用;看到 Google 支持 H5 就误以为 App 普通页面也能直接用。本节笔记只展开到“选择服务商、创建应用 Key”,后续具体配置不在本节范围内,不应凭空补写。

说明:提供的转写稿内容为“回车登记”组件、props、Pinia 等前端组件通信内容,与本节“地图服务”不匹配,因此不能据此补充地图服务推导,否则会编造。以上仅依据本节笔记扩写。

2.1 消息推送:本节范围与转写稿说明 ​

消息推送是服务端管理后台主动向安装了 App 的用户发送消息的功能。笔记明确把它分为离线推送和在线推送两种。本节的重点落在离线推送的配置流程上,在线推送没有展开,因此不能凭空补写在线推送的实现细节。

需要先说明一点:你提供的转写稿实际讲的是“神领物流-回车登记”里的组件封装、props、Pinia、v-model 等内容,和“2.1 消息推送”不是同一节。转写稿中出现的“主建/主件”应理解为“组件”,“pros”应理解为“props”,“死道”应理解为“store”,“批靡/拼你”应理解为“Pinia”,“微杠model”应理解为“v-model”。这些属于回车登记那一节的推导与调试过程,不应硬塞进消息推送本节,否则会编造出笔记中没有的推送调试步骤。因此下面只依据本节笔记扩写,并保留笔记中的图片与配置顺序;笔记没有给出代码片段,也不补造代码。

2.1.1 应用信息:先把应用身份补齐 ​

为什么需要:消息推送平台要识别“是哪一个 App 在申请推送”,所以要先完善平台信息。笔记要求填写 MD5、SHA1、SHA256 信息,iOS 平台信息也需要完善,但只需要填写一个包名。

怎么想:先按平台把 Android 侧需要的签名信息补齐,再补 iOS 侧包名,最后选择 uniPush 的版本。顺序上不要反过来,否则后面厂商推送配置时缺少应用身份信息。

怎么做:按图示完善平台信息,填写 MD5、SHA1、SHA256 信息:

应用信息平台填写界面

Android签名信息填写界面

iOS 平台信息只需要填写一个包名:

iOS包名填写界面

然后找到 uniPush,选择 2.0,笔记说明它支持全端推送:

uniPush选择2.0界面

易错点:MD5、SHA1、SHA256 是三类不同信息,不要混填;iOS 侧笔记只要求包名,不要自行增加笔记没有的字段;uniPush 要选 2.0,不要停留在旧版本选项上。

2.1.2 离线推送:厂商通道的配置思路 ​

为什么需要:离线推送即厂商推送设置。它的价值在于不需要在 App 中添加代码、不引入 SDK,也能实现消息推送功能。也就是说,推送能力主要依靠厂商通道和平台配置完成。

怎么想:笔记同时指出了弊端——要为不同品牌的手机分别设置参数。所以不能认为“配置一次华为就代表所有安卓品牌都通了”,而要按品牌分别准备厂商参数。

怎么做:以华为手机为例,先注册成为华为开发者账号,笔记给出的地址是 https://developer.huawei.com/consumer/cn/agconnect,需要完成身份认证;华为手机用户可以使用自身的华为账号登录。具体厂商配置可参考个推服务平台文档:https://docs.getui.com/getui/mobile/vendor/vendor_open/。

易错点:华为开发者账号要完成身份认证;不同品牌手机要分别设置参数;不要把“不引入 SDK”误解成“完全不用配置”,厂商参数仍然要填。

2.1.3 配置神领物流并重新打包 ​

为什么需要:前面准备的是应用信息和厂商推送参数,最终要落到“神领物流”这个应用的推送配置里,否则后台发送消息时没有可用的推送通道。

怎么想:配置完成后不是保存就结束,笔记特别写了“配置完成后重新打包”。这说明配置需要进入新的安装包再验证。

怎么做:按下图完成神领物流的推送配置:

神领物流推送配置界面

配置完成后重新打包。

易错点:只改配置不重新打包,容易继续用旧包测试;厂商参数、应用信息要对应到当前应用,不能拿别的项目参数混用。

2.1.4 发送消息与云手机测试 ​

为什么需要:配置的最终目的是能从后台发送消息并验证到达效果。笔记给出后台发送消息的界面:

发送消息界面

怎么想:测试时要让设备品牌和所配置的厂商通道匹配。没有安卓机的同学,笔记建议使用 WeTest 平台提供的云手机进行测试,地址是 https://wetest.qq.com/products/cloud-phone,并注意选择华为品牌的手机。

怎么做:在后台发送消息,按图操作;测试设备优先选择与已配置厂商参数一致的品牌。以本节为例,配置了华为相关参数,就选华为品牌云手机验证。

易错点:云手机品牌选错,可能验证不到对应厂商通道;配置后未重新打包就发送,结果不可靠;测试时要把“应用信息、厂商参数、重新打包、设备品牌”四项对齐。

本节小结 ​

本节要掌握的是消息推送的基本分类和离线推送配置链路:先完善应用信息(MD5、SHA1、SHA256;iOS 包名;uniPush 2.0),再做厂商推送设置(以华为为例注册开发者账号并认证),然后配置神领物流并重新打包,最后在后台发送消息并用华为品牌云手机测试。转写稿中的组件通信、Pinia、v-model 属于回车登记内容,不应作为本节推送推导;本节没有代码片段,故不补造代码。

说明:本节笔记标题写“2.2 一键登录”,但转写稿实际讲的是“回车登记”里的选项组件封装、选中高亮、Pinia 存数据与 v-model 回填;转写稿没有出现一键登录实现,下面按转写内容整理,不编造登录流程。转写稿未提供图片,故不新增图片。

先明确本节真实任务 ​

为什么需要:页面上方和下方的选项样式一开始不一致,根因不是样式文件坏了,而是下方还没复用同一个组件。怎么想:先把“一个小方块选项”抽成公共组件,再让三个业务区域都使用它。怎么做:找到原先写死的 options 结构,替换成封装后的 Option 组件,并给组件传入数组。易错点:只改样式不抽组件,后面还会重复乱;替换时先找类名定位,别把整块业务结构误删。

用 props 接收数组并 v-for 渲染 ​

为什么需要:组件不能把“闯红灯、无证驾驶”写死,否则事故、故障区域无法复用。怎么想:外部使用组件时传数组,组件内部用 props 接收,再 v-for 渲染。怎么做:

js
defineProps({
  types: { type: Array, required: true },
  dataKey: { type: String, required: true }
})
html
<div v-for="(option, index) in props.types" :key="option.ID">
  {{ option.text }}
</div>

易错点:props 里定义的名字,使用组件时才能写对应属性;数组单元后来从字符串改成对象,模板里必须从 option.text、option.ID 取值,不能还写 option。

点击事件、索引与 active 高亮 ​

为什么需要:点击选项要有反应,而且不能固定 true/false,否则所有项一起变色或都不变色。怎么想:点击时先拿到当前项索引,用 ref 记录“当前选中索引”,模板里比较 index === typeIndex 决定是否加 active。怎么做:先写点击事件 console.log 验证事件能触发,再固定 true/false 验证样式类,最后改成索引比较:

js
import { ref } from 'vue'
const typeIndex = ref(-1)

function onOptionSelect(index, id) {
  typeIndex.value = index
}
html
<div
  v-for="(option, index) in props.types"
  :key="option.ID"
  :class="{ active: index === typeIndex }"
  @click="onOptionSelect(index, option.ID)"
>
  {{ option.text }}
</div>

易错点:ref 忘记导入会导致热更新失败,页面像没反应,需手动刷新;初值给 0 会默认选中第一项,所以给 -1;索引和选中索引比较时别把 index 写成固定值。

用 Pinia 解决三层组件的数据通信 ​

为什么需要:最里层 Option 选中了数据,但最终提交在最外层页面,组件套了三层,靠父子 props 一层层传会很臃肿。怎么想:父子可用 props/事件,兄弟可找共同父级,但数据量大时用 Pinia 更合适。怎么做:定义 task store,把接口需要的字段统一放进 recordData:

js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useTaskStore = defineStore('task', () => {
  const recordData = ref({})
  return { recordData }
})

组件内:

js
const taskStore = useTaskStore()
taskStore.recordData[dataKey] = option.ID

易错点:return 的是 recordData,使用时就是 taskStore.recordData[...],不要再乱点 .value;dataKey 要与接口字段名对应;store 导出名、导入路径写错会直接报错。

存 ID 而不是文字,并用 v-model 回填 ​

为什么需要:页面显示“闯红灯”,但接口提交往往要数字 ID。怎么想:点击时同时拿索引和 option.ID,索引只管样式,ID 存进 store;文字可保留用于调试。怎么做:最终存 option.ID;文本域、图片等用 v-model 绑到 taskStore.recordData 的对应字段。易错点:存成 option.text 会导致接口参数不对;v-model 字段名写串、上传服务空间未关联、图片类型/数量限制没配,都会导致回填或上传失败。调试时先看 Vue 调试工具里的 Pinia 数据是否随点击变化,再查模板绑定。

代码示例 ​

1. 选项组件 sl-options(抽离复用,管理选中样式与数据回写) ​

vue
<!-- components/sl-options/sl-options.vue -->
<template>
  <view class="options">
    <view
      v-for="(option, index) in types"
      :key="option.id"
      class="option"
      :class="{ active: typeIndex === index }"
      @click="onOptionSelect(index, option)"
    >
      {{ option.text }}
    </view>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import { useTaskStore } from '@/stores/task'

// 接收外部传入的数组与数据名字
const props = defineProps({
  types: { type: Array, default: () => [] },
  dataKey: { type: String, default: '' }
})

const emit = defineEmits(['onOptionSelect'])

// 记录当前选中项的索引,-1 表示默认不选中任何一项
const typeIndex = ref(-1)
const taskStore = useTaskStore()

function onOptionSelect(index, option) {
  // 1. 记录索引,控制 active 类名
  typeIndex.value = index
  // 2. 把索引和整条数据抛给父组件(父组件可用于调试/打印)
  emit('onOptionSelect', index, option)
  // 3. 存进 Pinia:recordData[dataKey].value 存的是 id(接口要数值)
  taskStore.recordData[props.dataKey].value = option.id
}
</script>

<style lang="scss" scoped>
.options {
  display: flex;
  flex-wrap: wrap;
}
.option {
  padding: 6rpx 20rpx;
  margin: 0 16rpx 16rpx 0;
  border: 1rpx solid #cccccc;
  border-radius: 8rpx;
  color: #333333;
}
.option.active {
  color: #ffffff;
  background-color: #2979ff;
  border-color: #2979ff;
}
</style>

关键行与易错点

  • props 里定义成 dataKey,父组件使用时必须写成 data-key(驼峰 → 短横线),名字对不上就传不进去,这是本节最容易踩的坑。
  • typeIndex 初值给 -1 而不是 0:给 0 会默认选中第一个,索引不可能是负数,所以用 -1 表示“一个都没选”。
  • v-for 中同时拿到 index,用它和 typeIndex 比较来加 active 类名,这是“点谁谁变样式”的核心。
  • 存入的是 option.id(数值),不是 option.text;text 可保留在 emit 里方便调试。
  • ref 千万别忘记 import { ref } from 'vue'——漏掉会报错,并且会导致热更新失效,需要手动刷新页面才看得到效果。

2. Pinia Store:stores/task.js ​

js
// stores/task.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useTaskStore = defineStore('task', () => {
  // 字段名与接口字段保持一致(字段名全部来自接口文档)
  const recordData = ref({
    breakRulesType: { value: '' },        // 交通违章类型
    accident: { value: '' },              // 事故类型
    accidentDescription: { value: '' },   // 事故描述
    accidentImageList: { value: [] },     // 事故图片
    fault: { value: '' },                 // 故障类型
    faultDescription: { value: '' },      // 故障描述
    faultImageList: { value: [] }         // 故障图片
  })

  return { recordData }
})

关键行与易错点

  • 用 defineStore('task', () => {...}) 的 setup 写法,里面就相当于 setup,可以正常写 ref、computed。
  • 必须 return { recordData } 导出,否则组件里 taskStore.recordData 是 undefined。
  • recordData 是 ref,组件里通过 taskStore.recordData.xxx 访问即可,不要再写一层 .value——写成 taskStore.value.xxx 会直接报错、数据存不进去(这是视频里实际翻车过的地方)。
  • 每个字段用 { value: ... } 包一层,是为了后面表单/图片统一走 .value 提交,结构保持一致。

3. 页面中使用组件 + v-model 绑定 store ​

vue
<!-- pages/record/record.vue -->
<template>
  <view class="page">
    <!-- 交通违章:遍历时把数据名字 item.k 传进去 -->
    <view v-for="item in breakRuleList" :key="item.k">
      <sl-options :types="item.t" :data-key="item.k" />
    </view>

    <!-- 事故:只有一组,直接传字符串 -->
    <sl-options :types="accidentTypes" data-key="accident" />
    <textarea
      v-model="taskStore.recordData.accidentDescription.value"
      placeholder="请输入事故描述"
    />
    <uni-file-picker
      v-model="taskStore.recordData.accidentImageList.value"
      limit="3"
      file-mediatype="image"
    />

    <!-- 车辆故障 -->
    <sl-options :types="faultTypes" data-key="fault" />
    <textarea
      v-model="taskStore.recordData.faultDescription.value"
      placeholder="请输入故障描述"
    />
    <uni-file-picker
      v-model="taskStore.recordData.faultImageList.value"
      limit="3"
      file-mediatype="image"
    />
  </view>
</template>

<script setup>
import { ref } from 'vue'
import slOptions from '@/components/sl-options/sl-options.vue'
import { useTaskStore } from '@/stores/task'

const taskStore = useTaskStore()

// t 是选项数组,k 是对应接口的数据名字
const breakRuleList = ref([
  {
    k: 'breakRulesType',
    t: [
      { id: 1, text: '闯红灯' },
      { id: 2, text: '无证驾驶' }
    ]
  }
])

const accidentTypes = ref([
  { id: 1, text: '追尾事故' },
  { id: 2, text: '刮擦事故' }
])

const faultTypes = ref([
  { id: 1, text: '启动困难' },
  { id: 2, text: '不着车' }
])
</script>

关键行与易错点

  • 组件必须先 import 再使用;视频里就出现过“还没引入组件就直接写标签”的低级错误。
  • 数据从 { text } 改成 { id, text } 对象后,组件模板里取文字要用 option.text、:key 用 option.id,否则页面文字会“消失”。
  • :types="item.t" 是属性绑定(有冒号),data-key="accident" 是固定字符串(无冒号)——遍历场景用绑定,单组场景直接写死。
  • v-model 直接绑到 taskStore.recordData.xxx.value 上,输入框、图片选择器改动会同步进 store,最外层页面直接读 store 就能提交给后端。
  • 抽离组件后,父页面里原来的 typeIndex、点击事件、active 样式都成了“垃圾代码”,要清理掉,否则容易出现两套状态互相干扰。

常见错误 ​

1. 点击选项后全部变红 / 点哪个都不变色

  • 现象::class 加上了,但所有项同时高亮,或完全没有反应。
  • 原因:把 active 写死成 true/false,或用 index === 0 之类固定值判断,没有和"当前选中索引"比对。
  • 修法:用 ref(-1) 记录选中索引,模板写 :class="{ active: index === activeIndex }"。

2. 页面一进来第一项就默认选中

  • 现象:未做任何操作,首项已是高亮态。
  • 原因:记录索引的 ref 初值给了 0,而索引 0 恰好对应第一项。
  • 修法:初值给 -1(索引不可能为负),即可实现"默认不选中"。

3. 组件内渲染不出文字,v-for 一片空白

  • 现象:组件挂上了,但选项列表不显示,控制台常见 undefined。
  • 原因:props 定义的名字与使用处绑定的属性名不一致(如定义 types 却写了 :type),或使用组件时压根忘了传。
  • 修法:定义什么名字就绑什么名字;驼峰属性在模板中写成短横线(dataKey → data-key);用 devtools 展开该组件节点,直接看 props 的实际值。

4. 改完代码页面不刷新

  • 现象:保存后界面毫无变化,刷新一下又好了。
  • 原因:控制台已有报错(如漏了 import { ref }),报错导致热更新失败,报错被忽略。
  • 修法:先清掉控制台红字再刷新;凡是用到 ref/computed 的文件,检查 import 是否补齐。

5. 存进 store 的是文字,接口要的是数字

  • 现象:提交后后端收到"闯红灯"而不是 1。
  • 原因:写入时取了 option.text。
  • 修法:写入取 option.id,text 保留下来做调试打印即可。

6. 三个模块的数据串台

  • 现象:车辆故障页显示的却是事故的选项,或写进 store 的字段名对不上。
  • 原因:三个页面结构高度相似,复制后忘替换数据源和 data-key。
  • 修法:逐个核对"数组来源 / data-key / store 中的字段名"三者是否一致,字段名以接口文档为准。

7. 抽离组件后页面报"变量未定义"

  • 现象:组件替换完成,控制台报旧变量找不到。
  • 原因:原页面里旧的点击回调、activeIndex、ref 等残留代码没有清理。
  • 修法:组件替换后把页面里旧的交互逻辑整段删除,只保留数据定义与组件使用。

练习 ​

  1. 把选项的选中态从"单选"改成"可取消":再次点击已选中的项时取消高亮。(提示:比对 index 与 activeIndex,相等则置回 -1。)
  2. 给组件新增 disabled 属性,禁用状态下点击不写入 store、不改选中态。
  3. 通读三个模块的使用处,列一张自查表(数组来源 / data-key / store 路径三列),故意改错一处,记录报错信息与页面表现。
  4. 用 devtools 观察 recordDate,在三个模块各选一项并上传图片,写出最终对象的结构,再据此写出提交给后端时的参数组装代码。
  5. 为三处 v-model 绑定的数据加提交前校验:描述为空或图片为 0 张时提示并中止提交。

原视频对照 ​

#视频时长
1神领物流-回车登记-512:14
2神领物流-回车登记-609:30
3神领物流-回车登记-708:44
4神领物流-回车登记-811:02
5神领物流-回车登记-910:11
6神领物流-回车登记-1007:38
7神领物流-回车登记-1111:46

小结 ​

  1. 抽离组件的动因是"上下结构一致";抽离后所有使用处必须同步替换,否则样式必然错乱。
  2. props 是"定义在组件内、写在标签上"的成对关系,改了名字使用处必须跟着改,否则静默拿到 undefined,不报错。
  3. 选中态的本质是"记录索引 + 比对索引",索引初值取 -1 是最省事的"默认不选中"方案。
  4. 组件越公共,越要把"上下文"显式传进去(数据名),否则父级拿到值也无法归类。
  5. 数据结构要跟着接口走:页面显示用 text,提交用 id,两个都保留最稳。
  6. 三层嵌套不要用 props/emit 硬传,直接上 Pinia;store 的 return 结构决定访问路径,写之前先看清。
  7. v-model 直接绑 store 数据是最省事的双向同步方式,但路径层级必须写全。
  8. 热更新失效先看控制台红字,八成是漏 import 或变量名写错,不是框架问题。