Files
yxe-mp/util/store-picker.js
T
weicw-dshsa aff1e52e3e 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 共用一份,
  避免两页各自维护导致解析口径不一致
2026-10-11 22:50:15 +08:00

143 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 连锁 / 门店两级选择的公用逻辑。
*
* 抽出来的原因:驻店填报(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
}