feat(自主填报): 拆分驻店/跑店填报并新增跑店填报页
「自主填报」原本只有一种表单。本次拆成两种类型:
1. 新增选择填报类型页 packageUser/self-report/select
- 按设计稿:hero 页头 + 两张类型卡(驻店填报 / 跑店填报)+ 确认选择
- 入口(首页、我的)由直接进 create 改为先进 select
2. 原自主填报页改为「驻店填报」
- 仅改导航标题,表单内容不变
3. 新增跑店填报页 packageUser/self-report/visit
- 复制现有跑店/巡店任务(pages/task/detail 的 cateId=4 分支)的填报页并改造:
保留巡店列表与下方内容,去掉列表上方的任务卡片/总进度/任务详情;
打卡、摆台照、销量、小票、门店评分一并不保留
- 巡店列表内「连锁筛选」「门店筛选」是两个并列的独立输入框,
各自弹各自的选择器(不串联),样式与驻店填报逐项对齐;
换连锁会清空已选门店
- 每条巡店可新增/删除,独立获取定位 + 打卡拍照(最多 9 张)
- 定位不强制(巡店任务里是强制的,这里放开)
- 底部「保存草稿」+「提交填报」,草稿交互与驻店填报一致:
进页面询问是否加载、提交成功后清除;storage key 与驻店分开
- 驳回后重新提交复用同一页面(mode=resubmit)
4. 填报记录页加「驻店 / 跑店」类型切换
- 两表分表查询,各自分页;跑店卡片展示门店数、巡店列表(门店/定位/照片)
- 连锁与门店关键词筛选落到巡店列表的每一行
5. 药审核加「跑店填报」类型
- 新增 AuditVisitReportItem,展开时按 id 拉详情(列表不下发 store_list)
- 跑店没有销量小计,审核通过需手填奖金,故补奖金输入弹层
顺带重构:
- 门店解析(扁平列表 / 连锁嵌套两种结构的兼容、门店明细不入页面 data
的性能处理)抽到 util/store-picker,create 与 visit 共用一份,
避免两页各自维护导致解析口径不一致
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="self-report-page">
|
||||
<AppNavbar :title="isResubmit ? '重新提交填报' : '自主填报'" showBack/>
|
||||
<AppNavbar :title="isResubmit ? '重新提交填报' : '驻店填报'" showBack/>
|
||||
|
||||
<view class="sr-merchant-row" @tap="openChainPicker">
|
||||
<view class="sr-merchant-label">连锁筛选</view>
|
||||
@@ -268,6 +268,15 @@ import {
|
||||
clearSalesHandoff,
|
||||
cloneSalesItems
|
||||
} from "@/util/sales-handoff";
|
||||
import {
|
||||
normalizeMerchant as sharedNormalizeMerchant,
|
||||
pickFirstText as sharedPickFirstText,
|
||||
unwrapList as sharedUnwrapList,
|
||||
normalizeMerchantChain as sharedNormalizeMerchantChain,
|
||||
groupMerchantListByChain as sharedGroupMerchantListByChain,
|
||||
flattenMerchantsFromChains as sharedFlattenMerchantsFromChains,
|
||||
filterMerchantStores as sharedFilterMerchantStores
|
||||
} from "@/util/store-picker";
|
||||
|
||||
export default {
|
||||
components: {Login, AppNavbar},
|
||||
@@ -761,123 +770,36 @@ export default {
|
||||
this.currentCity = location.city_str || ''
|
||||
this.currentAddress = location.full || [location.province_str, location.city_str, location.area_str].filter(Boolean).join('')
|
||||
},
|
||||
// 连锁/门店解析统一走 util/store-picker:跑店填报(visit.vue)用同一份逻辑,
|
||||
// 两页各自维护一份迟早会出现解析口径不一致。
|
||||
normalizeMerchant(item) {
|
||||
const id = item.id ?? item.merchantId ?? item.merchant_id
|
||||
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
|
||||
const lat = Number(item.lat ?? item.latitude ?? item.merchantLat ?? item.merchant_lat)
|
||||
const lng = Number(item.lng ?? item.longitude ?? item.merchantLng ?? item.merchant_lng)
|
||||
const address = [item.province, item.city, item.region, item.area, item.address].filter(Boolean).join('')
|
||||
return {
|
||||
id,
|
||||
text,
|
||||
chainId: item.chainId ?? item.chain_id ?? '',
|
||||
chainName: item.chainName || item.chain || item.chainAbb || '',
|
||||
lat: Number.isFinite(lat) ? lat : null,
|
||||
lng: Number.isFinite(lng) ? lng : null,
|
||||
province: item.province || item.province_str || '',
|
||||
city: item.city || item.city_str || '',
|
||||
address
|
||||
}
|
||||
return sharedNormalizeMerchant(item)
|
||||
},
|
||||
pickFirstText(source, keys = []) {
|
||||
return sharedPickFirstText(source, keys)
|
||||
},
|
||||
unwrapList(data) {
|
||||
return sharedUnwrapList(data)
|
||||
},
|
||||
normalizeMerchantChain(item, index) {
|
||||
return sharedNormalizeMerchantChain(item, index)
|
||||
},
|
||||
groupMerchantListByChain(list) {
|
||||
return sharedGroupMerchantListByChain(list)
|
||||
},
|
||||
flattenMerchantsFromChains(chainList) {
|
||||
return sharedFlattenMerchantsFromChains(chainList)
|
||||
},
|
||||
// 本页的门店搜索关键词存在 data 里(merchantStoreSearchKeyword),
|
||||
// 与 visit.vue 由调用方传入关键词的签名不同,这里保留本页签名。
|
||||
filterMerchantStores(merchants) {
|
||||
return sharedFilterMerchantStores(merchants, this.merchantStoreSearchKeyword)
|
||||
},
|
||||
normalizeChainIdForApi(rawChainId) {
|
||||
const text = String(rawChainId || '').trim()
|
||||
if (!text || text.startsWith('chain_')) return ''
|
||||
return text
|
||||
},
|
||||
pickFirstText(source, keys = []) {
|
||||
if (!source || typeof source !== 'object') return ''
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
const value = source[keys[i]]
|
||||
if (value !== undefined && value !== null && String(value).trim()) {
|
||||
return String(value).trim()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
},
|
||||
unwrapList(data) {
|
||||
if (Array.isArray(data)) return data
|
||||
if (!data || typeof data !== 'object') return []
|
||||
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
|
||||
for (let i = 0; i < keys.length; i += 1) {
|
||||
const value = data[keys[i]]
|
||||
if (Array.isArray(value)) return value
|
||||
if (value && typeof value === 'object') {
|
||||
const nested = this.unwrapList(value)
|
||||
if (nested.length) return nested
|
||||
}
|
||||
}
|
||||
return [data]
|
||||
},
|
||||
normalizeMerchantChain(item, index) {
|
||||
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
|
||||
const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? chainName
|
||||
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
|
||||
? `chain_${index}`
|
||||
: rawChainId
|
||||
const childCandidates = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
|
||||
let rawChildren = []
|
||||
for (let i = 0; i < childCandidates.length; i += 1) {
|
||||
const list = item[childCandidates[i]]
|
||||
if (Array.isArray(list) && list.length) {
|
||||
rawChildren = list
|
||||
break
|
||||
}
|
||||
}
|
||||
let merchants = rawChildren
|
||||
.map(child => this.normalizeMerchant(child))
|
||||
.filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
|
||||
.map(merchant => Object.assign({}, merchant, {chainId, chainName}))
|
||||
if (!merchants.length) {
|
||||
const selfMerchant = this.normalizeMerchant(item)
|
||||
if (selfMerchant.id !== undefined && selfMerchant.id !== null && String(selfMerchant.id).trim() !== '') {
|
||||
merchants = [Object.assign({}, selfMerchant, {chainId, chainName})]
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: chainId,
|
||||
text: chainName,
|
||||
merchants
|
||||
}
|
||||
},
|
||||
groupMerchantListByChain(list) {
|
||||
const chainMap = {}
|
||||
;(Array.isArray(list) ? list : []).forEach((item, index) => {
|
||||
const merchant = this.normalizeMerchant(item)
|
||||
if (merchant.id === undefined || merchant.id === null || String(merchant.id).trim() === '') return
|
||||
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'chain_name'])
|
||||
if (!chainName) return
|
||||
const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? chainName
|
||||
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
|
||||
? `chain_${index}`
|
||||
: rawChainId
|
||||
const key = String(chainId)
|
||||
if (!chainMap[key]) {
|
||||
chainMap[key] = {id: chainId, text: chainName, merchants: []}
|
||||
}
|
||||
chainMap[key].merchants.push(Object.assign({}, merchant, {chainId, chainName}))
|
||||
})
|
||||
return Object.keys(chainMap).map(key => chainMap[key])
|
||||
},
|
||||
flattenMerchantsFromChains(chainList) {
|
||||
const list = []
|
||||
;(Array.isArray(chainList) ? chainList : []).forEach(chain => {
|
||||
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
|
||||
merchants.forEach(merchant => {
|
||||
if (!merchant || merchant.id === '' || merchant.id === null || merchant.id === undefined) return
|
||||
list.push(Object.assign({}, merchant, {
|
||||
chainId: merchant.chainId || chain.id || '',
|
||||
chainName: merchant.chainName || chain.text || ''
|
||||
}))
|
||||
})
|
||||
})
|
||||
return list
|
||||
},
|
||||
filterMerchantStores(merchants) {
|
||||
const list = Array.isArray(merchants) ? merchants : []
|
||||
const keyword = (this.merchantStoreSearchKeyword || '').trim()
|
||||
if (!keyword) return list
|
||||
return list.filter(merchant => (merchant.text || '').includes(keyword))
|
||||
},
|
||||
applyMerchant(chain, merchant) {
|
||||
if (!merchant) return
|
||||
this.form.chainName = (chain && chain.text) || merchant.chainName || ''
|
||||
|
||||
Reference in New Issue
Block a user