From 40fed38f8309d60875849e6163032df583b733f4 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Mon, 12 Oct 2026 18:18:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=B0=8F=E7=A8=8B=E5=BA=8F):=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20v-for=20:key=20=E8=A1=A8=E8=BE=BE=E5=BC=8F=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=EF=BC=8C=E6=81=A2=E5=A4=8D=E5=88=97=E8=A1=A8=20wx:key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 非 h5 平台的 :key 只接受简单属性名,模板字面量与运算表达式会被编译器 整条丢弃(uni-template-compiler 的 getForKey:Identifier 走 *this 或原名, MemberExpression 取属性名,其余一律 warning + 返回空串)。 所以这批告警不只是文案问题:产物里对应的 后面确实没有 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 数量逐文件相等。 --- packageUser/chronic/index.vue | 2 +- packageUser/self-report/create.vue | 16 ++++++++++------ packageUser/self-report/records.vue | 18 +++++++++--------- packageUser/self-report/sales.vue | 6 ++++-- packageUser/travel/index.vue | 4 ++-- pages/audit/components/AuditMovementItem.vue | 2 +- .../audit/components/AuditSelfReportItem.vue | 14 +++++++------- pages/audit/components/AuditShareItem.vue | 2 +- .../audit/components/AuditStoreVisitItem.vue | 2 +- .../audit/components/AuditVisitReportItem.vue | 8 +++++--- pages/share/detail.vue | 2 +- pages/task/detail.vue | 19 ++++++++++++++----- pages/visit/index.vue | 4 ++-- 13 files changed, 58 insertions(+), 41 deletions(-) diff --git a/packageUser/chronic/index.vue b/packageUser/chronic/index.vue index 911033d..bbee4fd 100644 --- a/packageUser/chronic/index.vue +++ b/packageUser/chronic/index.vue @@ -291,7 +291,7 @@ {{ item.text }} diff --git a/packageUser/self-report/create.vue b/packageUser/self-report/create.vue index b6e6faf..1fc89f2 100644 --- a/packageUser/self-report/create.vue +++ b/packageUser/self-report/create.vue @@ -59,7 +59,7 @@ 最多{{ boothLimit }}张 - + x @@ -95,7 +95,7 @@ 请点击上方进入「销量填写」添加商品 - + {{ item.name }} {{ formatSalesQty(item.qty) }}{{ item.unit || '盒' }} @@ -119,7 +119,7 @@ 最多{{ receiptLimit }}张 - + x @@ -201,7 +201,7 @@ @change="onChainPickerChange" > - + {{ item.text }} @@ -232,7 +232,7 @@ @change="onMerchantPickerChange" > - + {{ item.text }} @@ -360,7 +360,11 @@ export default { }, salesSummaryRows() { const items = Array.isArray(this.salesItems) ? this.salesItems : [] - return items.slice(0, this.salesSummaryLimit) + // wx:key 需要稳定唯一标识;销量项正常都带 _key(销量页生成/重提回填), + // 旧草稿缺失时按序号兜底,避免出现重复 key + return items.slice(0, this.salesSummaryLimit).map((item, index) => Object.assign({}, item, { + _key: (item && item._key) ? item._key : `sales_summary_${index}` + })) }, totalRewardAmount() { return (Array.isArray(this.salesItems) ? this.salesItems : []).reduce((sum, item) => { diff --git a/packageUser/self-report/records.vue b/packageUser/self-report/records.vue index 02423c6..41ad1c8 100644 --- a/packageUser/self-report/records.vue +++ b/packageUser/self-report/records.vue @@ -90,7 +90,7 @@ 暂无巡店记录 - + 巡店{{ sIdx + 1 }} {{ store.visitTime || '-' }} @@ -104,7 +104,7 @@ 销量填写 - + {{ detail.name }} {{ detail.qty }}{{ detail.unit }} @@ -427,7 +427,8 @@ export default { const summaryRaw = this.parseMaybeJson(item.summary, item.summary || '') const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw || '') const storeList = this.parseMaybeJson(item.storeList, []) - const storeRows = (Array.isArray(storeList) ? storeList : []).map(store => ({ + const storeRows = (Array.isArray(storeList) ? storeList : []).map((store, sIdx) => ({ + _key: `store_${sIdx}`, merchantName: store && (store.merchantName || store.storeName) ? (store.merchantName || store.storeName) : '', chainName: store && store.chainName ? store.chainName : '', visitTime: store && (store.visitTime || store.time) ? (store.visitTime || store.time) : '', @@ -467,8 +468,7 @@ export default { const items = Array.isArray(group && group.items) ? group.items : [] items.forEach((detail, iIdx) => { rows.push({ - key: `${gIdx}-${iIdx}`, - groupTitle: group && group.title ? group.title : `分组${gIdx + 1}`, + key: `${gIdx}-${iIdx}`, groupTitle: group && group.title ? group.title : `分组${gIdx + 1}`, name: detail && detail.name ? detail.name : '-', makerAbb: detail && detail.makerAbb ? detail.makerAbb : '', spec: detail && detail.spec ? detail.spec : '', diff --git a/packageUser/self-report/sales.vue b/packageUser/self-report/sales.vue index bd0a574..baaed4c 100644 --- a/packageUser/self-report/sales.vue +++ b/packageUser/self-report/sales.vue @@ -46,7 +46,7 @@ - + {{ formatBadge(row.index) }} @@ -261,7 +261,9 @@ export default { const keyword = (this.keyword || '').trim().toLowerCase() const cate = this.filterCate return (Array.isArray(this.salesItems) ? this.salesItems : []) - .map((item, index) => ({item, index})) + // wx:key 只认 item 上的属性名,表达式会被编译丢弃; + // 这里把 key 提到包装对象顶层,_key 缺失时用序号兜底保证唯一 + .map((item, index) => ({item, index, key: (item && item._key) || `sales_row_${index}`})) .filter(row => { if (cate && String(row.item.groupTitle || '') !== cate) return false if (!keyword) return true diff --git a/packageUser/travel/index.vue b/packageUser/travel/index.vue index eba670b..a66f3bc 100644 --- a/packageUser/travel/index.vue +++ b/packageUser/travel/index.vue @@ -64,7 +64,7 @@ 必填 - + x @@ -97,7 +97,7 @@ 备注:{{ item.remark || '-' }} 凭证: - + diff --git a/pages/audit/components/AuditSelfReportItem.vue b/pages/audit/components/AuditSelfReportItem.vue index 35e96d7..2c10b2c 100644 --- a/pages/audit/components/AuditSelfReportItem.vue +++ b/pages/audit/components/AuditSelfReportItem.vue @@ -26,7 +26,7 @@ 修改销量 - + {{ detail.name }} 销量:{{ detail.qty }}{{ detail.unit }} @@ -125,7 +125,7 @@ 暂无销量商品 - + {{ detail.name || '商品' }} @@ -199,12 +199,12 @@ - + {{ chain.text }} - + {{ merchant.text }} diff --git a/pages/audit/components/AuditShareItem.vue b/pages/audit/components/AuditShareItem.vue index 3d22eab..3139548 100644 --- a/pages/audit/components/AuditShareItem.vue +++ b/pages/audit/components/AuditShareItem.vue @@ -293,7 +293,7 @@ diff --git a/pages/audit/components/AuditStoreVisitItem.vue b/pages/audit/components/AuditStoreVisitItem.vue index ae8d3ed..0f047e7 100644 --- a/pages/audit/components/AuditStoreVisitItem.vue +++ b/pages/audit/components/AuditStoreVisitItem.vue @@ -63,7 +63,7 @@ diff --git a/pages/audit/components/AuditVisitReportItem.vue b/pages/audit/components/AuditVisitReportItem.vue index d8cd946..58b3c1e 100644 --- a/pages/audit/components/AuditVisitReportItem.vue +++ b/pages/audit/components/AuditVisitReportItem.vue @@ -13,7 +13,7 @@ 加载中... 暂无巡店记录 - + 巡店{{ idx + 1 }} {{ store.visitTime || '-' }} @@ -27,7 +27,7 @@ ({ + return (Array.isArray(list) ? list : []).map((store, sIdx) => ({ + // wx:key 只认属性名,表达式会被编译丢弃;巡店记录本身没有 id,按序号补一个 + _key: `store_${sIdx}`, merchantName: store && (store.merchantName || store.storeName) ? (store.merchantName || store.storeName) : '', chainName: store && store.chainName ? store.chainName : '', visitTime: store && (store.visitTime || store.time) ? (store.visitTime || store.time) : '', diff --git a/pages/share/detail.vue b/pages/share/detail.vue index 71d95ed..7d9fd7e 100644 --- a/pages/share/detail.vue +++ b/pages/share/detail.vue @@ -36,7 +36,7 @@ 目标{{ totalTaskNum }}家 暂无巡店记录 - + 巡店{{ idx + 1 }} @@ -183,7 +183,7 @@ 暂无巡店照片 - + x @@ -451,12 +451,12 @@ @change="onMerchantPickerChange" > - + {{ item.text }} - + {{ item.text }} @@ -498,7 +498,7 @@ @@ -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) : '', diff --git a/pages/visit/index.vue b/pages/visit/index.vue index db43afe..408abf2 100644 --- a/pages/visit/index.vue +++ b/pages/visit/index.vue @@ -157,7 +157,7 @@ - + x @@ -217,7 +217,7 @@ 活动小结:{{ item.summary || '-' }} 照片: - +