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:
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 连锁 / 门店两级选择的公用逻辑。
|
||||
*
|
||||
* 抽出来的原因:驻店填报(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
|
||||
}
|
||||
Reference in New Issue
Block a user