主题
在途管理与打包发布(二)
本页对应 在途管理与打包发布,共 7 段视频,约 1 小时 11 分。
学习目标
- 能够将重复的选项区域抽离为公共组件,并通过
defineProps定义types、dataKey等属性,在父组件中使用属性绑定传入数组和字符串数据。 - 能够使用
v-for遍历传入的选项数组,并通过点击事件同时传递选项索引和选项对象,动态判断并添加active类名实现选中高亮。 - 能够定义并导出 Pinia store,在组件中调用
useTaskStore,按选项的dataKey将用户选择的id记录到recordData对应字段中。 - 能够使用
v-model将事故描述、故障描述、图片列表等表单数据绑定到 Pinia store 中的对应字段,并验证选择、输入和上传后数据可被正确读取。 - 能够根据后端接口要求将选项数据改造为包含
text与id的对象结构,页面展示text、提交时记录id。 - 能够清理抽离组件后原页面中冗余的选中索引、事件处理和无效状态代码,保证组件复用后三个业务区域交互一致。
知识点
本节要解决的问题
在 uni-app 中使用地图服务,不能直接假设“一个地图服务商 everywhere 都能用”。因为 uni-app 要同时面对 App、H5、小程序等不同平台,而不同平台对地图 SDK 的支持范围并不一致。本节先解决“选哪家地图服务商”和“如何创建应用 Key”的问题,为后续地图能力接入做准备。
笔记给出的兼容性表如下:
| 地图服务商 | App | H5 | 小程序 |
|---|---|---|---|
| 高德 | √ | √ | |
| 仅nvue页面 | √ | ||
| 腾讯 | √ | √ |
从表中可以看出:H5 和小程序平台选择腾讯地图服务平台,App 选择高德地图服务平台。这样做的原因是各平台支持情况不同,不能只看“地图服务商是否知名”,而要看它在目标平台是否可用。Google 地图在 App 端仅支持 nvue 页面,普通 vue 页面不能直接按同样方式使用,因此也不作为本项目 App 端首选。
选择服务商后要创建应用 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 信息:


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

然后找到 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等残留代码没有清理。 - 修法:组件替换后把页面里旧的交互逻辑整段删除,只保留数据定义与组件使用。
练习
- 把选项的选中态从"单选"改成"可取消":再次点击已选中的项时取消高亮。(提示:比对
index与activeIndex,相等则置回-1。) - 给组件新增
disabled属性,禁用状态下点击不写入 store、不改选中态。 - 通读三个模块的使用处,列一张自查表(数组来源 /
data-key/ store 路径三列),故意改错一处,记录报错信息与页面表现。 - 用 devtools 观察
recordDate,在三个模块各选一项并上传图片,写出最终对象的结构,再据此写出提交给后端时的参数组装代码。 - 为三处
v-model绑定的数据加提交前校验:描述为空或图片为 0 张时提示并中止提交。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 神领物流-回车登记-5 | 12:14 |
| 2 | 神领物流-回车登记-6 | 09:30 |
| 3 | 神领物流-回车登记-7 | 08:44 |
| 4 | 神领物流-回车登记-8 | 11:02 |
| 5 | 神领物流-回车登记-9 | 10:11 |
| 6 | 神领物流-回车登记-10 | 07:38 |
| 7 | 神领物流-回车登记-11 | 11:46 |
小结
- 抽离组件的动因是"上下结构一致";抽离后所有使用处必须同步替换,否则样式必然错乱。
- props 是"定义在组件内、写在标签上"的成对关系,改了名字使用处必须跟着改,否则静默拿到
undefined,不报错。 - 选中态的本质是"记录索引 + 比对索引",索引初值取
-1是最省事的"默认不选中"方案。 - 组件越公共,越要把"上下文"显式传进去(数据名),否则父级拿到值也无法归类。
- 数据结构要跟着接口走:页面显示用
text,提交用id,两个都保留最稳。 - 三层嵌套不要用 props/emit 硬传,直接上 Pinia;store 的
return结构决定访问路径,写之前先看清。 v-model直接绑 store 数据是最省事的双向同步方式,但路径层级必须写全。- 热更新失效先看控制台红字,八成是漏 import 或变量名写错,不是框架问题。