aff1e52e3e
「自主填报」原本只有一种表单。本次拆成两种类型:
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 共用一份,
避免两页各自维护导致解析口径不一致
143 lines
5.6 KiB
JavaScript
143 lines
5.6 KiB
JavaScript
/**
|
||
* 连锁 / 门店两级选择的公用逻辑。
|
||
*
|
||
* 抽出来的原因:驻店填报(packageUser/self-report/create.vue)与跑店填报
|
||
* (packageUser/self-report/visit.vue)都要「先选连锁、再选门店」,
|
||
* 且门店全量可达 8000+ 条 —— 明细必须挂在实例上而不是页面 data,
|
||
* 否则页面 data 树膨胀到 1MB+,每次 setData 的 diff/序列化都会从 ~490ms 涨到 ~1000ms。
|
||
*
|
||
* 用法:在页面 methods 里混入本文件的 methods(页面 data 仍需自己声明
|
||
* merchantChainList / merchantLoading / merchantStoreSearchKeyword / 各 picker 的开关与 value)。
|
||
*/
|
||
|
||
export function 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 ''
|
||
}
|
||
|
||
export function 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 = unwrapList(value)
|
||
if (nested.length) return nested
|
||
}
|
||
}
|
||
return [data]
|
||
}
|
||
|
||
export function 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
|
||
}
|
||
}
|
||
|
||
export function normalizeMerchantChain(item, index) {
|
||
const chainName = 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 => 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 = 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
|
||
}
|
||
}
|
||
|
||
// 接口可能返回「扁平门店列表(带 chainName)」,也可能返回「连锁嵌套门店」,两种都兼容
|
||
export function groupMerchantListByChain(list) {
|
||
const chainMap = {}
|
||
;(Array.isArray(list) ? list : []).forEach((item, index) => {
|
||
const merchant = normalizeMerchant(item)
|
||
if (merchant.id === undefined || merchant.id === null || String(merchant.id).trim() === '') return
|
||
const chainName = 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])
|
||
}
|
||
|
||
export function 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
|
||
}
|
||
|
||
// 门店关键词过滤:连锁名命中由调用方另判,这里只按门店名匹配
|
||
export function filterMerchantStores(merchants, keyword) {
|
||
const list = Array.isArray(merchants) ? merchants : []
|
||
const text = String(keyword || '').trim()
|
||
if (!text) return list
|
||
return list.filter(merchant => (merchant.text || '').includes(text))
|
||
}
|
||
|
||
export default {
|
||
pickFirstText,
|
||
unwrapList,
|
||
normalizeMerchant,
|
||
normalizeMerchantChain,
|
||
groupMerchantListByChain,
|
||
flattenMerchantsFromChains,
|
||
filterMerchantStores
|
||
}
|