fix(小程序): 修正 v-for :key 表达式告警,恢复列表 wx:key

非 h5 平台的 :key 只接受简单属性名,模板字面量与运算表达式会被编译器
整条丢弃(uni-template-compiler 的 getForKey:Identifier 走 *this 或原名,
MemberExpression 取属性名,其余一律 warning + 返回空串)。

所以这批告警不只是文案问题:产物里对应的 <block wx:for> 后面确实没有
wx:key,列表项因此失去复用标识,增删时会整段重建节点。本次按编译器
接受的写法逐一改正,共 13 个文件。

改法:
- 纯 URL 字符串数组(打卡/门头/小票/凭证/巡店照片)直接用元素自身
  :key="img",编译为 wx:key="*this"
- 对象数组用其已有唯一字段:连锁/门店 item.id、抄送用户 user.id、
  审计销量行 detail.key、编辑销量项 detail._key
- 本身无唯一字段的巡店列表补 _key:task/detail 沿用 visit.vue 已验证的
  _rowKey(自增 seed,增删稳定),records 与 AuditVisitReportItem 用序号

sales.vue 另有一处隐蔽问题:row 是 {item, index} 包装对象,
:key="row.item._key || row.index" 即便写成简单形式也会生成 wx:key="_key",
在 row 上取不到值。改为在 visibleSalesItems 里把 key 提到包装对象顶层,
_key 缺失时用序号兜底。create.vue 的销量摘要同样补 __key 兜底,避免重复 key。

注:task/detail 的 storeList 提交走 sanitizeStoreList() 显式白名单,
_rowKey 不会外泄到后端。

验证:HBuilderX cli 重新编译 —— 编译成功,非 h5 平台 :key 告警 0 条
(修改前满屏 40+ 条),产物中 wx:for 与 wx:key 数量逐文件相等。
This commit is contained in:
2026-10-12 18:18:11 +08:00
parent e192296ea7
commit 40fed38f83
13 changed files with 58 additions and 41 deletions
+14 -5
View File
@@ -126,7 +126,7 @@
<view class="section-extra" v-if="totalTaskNum">目标{{ totalTaskNum }}家</view>
</view>
<view v-if="!storeList.length" class="audit-empty-tip">暂无巡店记录</view>
<view v-for="(item, idx) in storeList" :key="`store-${idx}`" class="visit-store-card">
<view v-for="(item, idx) in storeList" :key="item._rowKey" class="visit-store-card">
<view class="visit-store-head">
<view>
<view class="visit-store-title">巡店{{ idx + 1 }}</view>
@@ -183,7 +183,7 @@
<view v-else class="audit-empty-tip visit-store-empty">暂无巡店照片</view>
</view>
<view v-else class="photo-grid">
<view v-for="(photo, photoIdx) in item.photos" :key="`store-${idx}-photo-${photoIdx}`" class="photo-item">
<view v-for="(photo, photoIdx) in item.photos" :key="photo" class="photo-item">
<image :src="photo" mode="aspectFill" class="photo-image" @tap="previewStorePhotos(idx, photoIdx)"/>
<view v-if="!isAuditEntry" class="photo-remove" @tap="removeStorePhoto(idx, photoIdx)">x</view>
</view>
@@ -451,12 +451,12 @@
@change="onMerchantPickerChange"
>
<picker-view-column>
<view v-for="(item, idx) in merchantChainOptions" :key="`chain-${idx}`" class="merchant-picker-item">
<view v-for="(item, idx) in merchantChainOptions" :key="item.id" class="merchant-picker-item">
{{ item.text }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="(item, idx) in merchantStoreOptions" :key="`merchant-${idx}`" class="merchant-picker-item">
<view v-for="(item, idx) in merchantStoreOptions" :key="item.id" class="merchant-picker-item">
{{ item.text }}
</view>
</picker-view-column>
@@ -498,7 +498,7 @@
<scroll-view v-else class="audit-copy-user-list" scroll-y>
<view
v-for="user in auditCopyUsers"
:key="`audit-copy-user-${user.id}`"
:key="user.id"
:class="['audit-copy-user-item', auditSelectedCopyUserId === user.id ? 'audit-copy-user-item-active' : '']"
@tap="auditSelectedCopyUserId = user.id"
>
@@ -1046,6 +1046,9 @@ export default {
},
createStoreItem(merchant = null, autoSelected = true) {
return Object.assign({
// 小程序 wx:key 只认属性名,模板字面量会被编译丢弃;
// 该字段同时让增删巡店时的节点复用稳定(删除靠后的项不会让前面的项重建)
_rowKey: this.buildStoreKey(),
visitTime: this.formatDateTime(new Date()),
lat: '',
lng: '',
@@ -1053,11 +1056,17 @@ export default {
photos: []
}, this.buildStoreMerchantFields(merchant, autoSelected))
},
// wx:key 需要稳定且互不相同的标识;用自增序号,避免同一份数据里出现重复 key
buildStoreKey() {
this.storeKeySeed = (this.storeKeySeed || 0) + 1
return `store_${this.storeKeySeed}`
},
normalizeStoreItem(item) {
const photos = Array.isArray(item && item.photos)
? item.photos.filter(Boolean)
: this.normalizeReceiptPhotos(item && (item.photos || item.images || item.image))
return {
_rowKey: item && item._rowKey ? item._rowKey : this.buildStoreKey(),
merchantId: item && (item.merchantId ?? item.storeId ?? item.id) !== undefined
? (item.merchantId ?? item.storeId ?? item.id)
: '',