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:
@@ -3,6 +3,16 @@
|
||||
<AppNavbar title="填报记录" showBack/>
|
||||
|
||||
<view class="card subsection">
|
||||
<view class="type-tabs">
|
||||
<view
|
||||
v-for="tab in typeTabs"
|
||||
:key="tab.value"
|
||||
:class="['tab', activeType === tab.value ? 'active' : '']"
|
||||
@tap="changeType(tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="status-tabs">
|
||||
<view
|
||||
v-for="tab in statusTabs"
|
||||
@@ -45,21 +55,21 @@
|
||||
<view v-for="item in filteredRecords" :key="item.id" class="record-card">
|
||||
<view class="record-head">
|
||||
<view>
|
||||
<view class="record-title">{{ item.merchantName || '-' }}</view>
|
||||
<view class="record-sub" v-if="item.chainName && item.chainName !== '-'">连锁:{{ item.chainName }}</view>
|
||||
<view class="record-title">{{ cardTitle(item) }}</view>
|
||||
<view class="record-sub" v-if="cardSubTitle(item)">{{ cardSubTitle(item) }}</view>
|
||||
</view>
|
||||
<view :class="['status-badge', statusClass(item.status)]">{{ statusText(item.status) }}</view>
|
||||
</view>
|
||||
<view class="record-grid">
|
||||
<view class="record-kv">
|
||||
<view v-if="activeType === 'store'" class="record-kv">
|
||||
<text class="record-label">活动时间</text>
|
||||
<text class="record-value">{{ item.activityTime || '-' }}</text>
|
||||
</view>
|
||||
<!-- <view class="record-kv">-->
|
||||
<!-- <text class="record-label">奖金</text>-->
|
||||
<!-- <text class="record-value text-red">¥{{ formatPrice(item.reward) }}</text>-->
|
||||
<!-- </view>-->
|
||||
<view class="record-kv">
|
||||
<view v-else class="record-kv">
|
||||
<text class="record-label">巡店门店数</text>
|
||||
<text class="record-value">{{ item.storeCount || 0 }}家</text>
|
||||
</view>
|
||||
<view v-if="activeType === 'store'" class="record-kv">
|
||||
<text class="record-label">评分</text>
|
||||
<text class="record-value">{{ item.rating || '-' }}星</text>
|
||||
</view>
|
||||
@@ -77,6 +87,35 @@
|
||||
<u-icon :name="isExpanded(item.id) ? 'arrow-up' : 'arrow-down'" size="20" color="#2fc6a7"/>
|
||||
</view>
|
||||
<view v-if="isExpanded(item.id)" class="record-detail">
|
||||
<!-- 跑店填报:巡店列表(驻店填报没有门店列表,走下面的固定区块) -->
|
||||
<block v-if="activeType === 'visit'">
|
||||
<view v-if="!item.storeRows.length" class="detail-empty">暂无巡店记录</view>
|
||||
<view v-for="(store, sIdx) in item.storeRows" :key="`store-${sIdx}`" class="store-block">
|
||||
<view class="store-block-head">
|
||||
<text class="store-block-title">巡店{{ sIdx + 1 }}</text>
|
||||
<text class="store-block-time">{{ store.visitTime || '-' }}</text>
|
||||
</view>
|
||||
<view class="store-block-line">门店:{{ store.merchantName || '-' }}</view>
|
||||
<view class="store-block-line">连锁:{{ store.chainName || '-' }}</view>
|
||||
<view class="store-block-line">
|
||||
定位:{{ store.location || '-' }}
|
||||
<text v-if="store.lat && store.lng">({{ store.lat }}, {{ store.lng }})</text>
|
||||
</view>
|
||||
<view v-if="store.photos.length" class="media-images">
|
||||
<image
|
||||
v-for="(img, imgIdx) in store.photos"
|
||||
:key="`store-${sIdx}-photo-${imgIdx}`"
|
||||
class="media-image"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
@tap="preview(img, store.photos)"
|
||||
/>
|
||||
</view>
|
||||
<view v-else class="detail-empty">暂无巡店照片</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block v-else>
|
||||
<view class="record-section">
|
||||
<view class="record-section-title">上下班打卡</view>
|
||||
<view v-if="item.clockImages.length" class="media-images">
|
||||
@@ -139,6 +178,7 @@
|
||||
</view>
|
||||
<view v-else class="detail-empty">暂无销量数据</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<view v-if="isRejected(item.status)" class="record-actions">
|
||||
@@ -185,12 +225,19 @@ import AppNavbar from "@/components/AppNavbar.vue";
|
||||
import Login from "@/components/Login.vue";
|
||||
import {dictDataListByDictReq} from "@/api/common";
|
||||
import {selfReportUserListReq} from "@/api/self-report";
|
||||
import {visitReportUserListReq} from "@/api/visit-report";
|
||||
import {getUserInfo} from "@/util/user";
|
||||
|
||||
export default {
|
||||
components: {AppNavbar, Login},
|
||||
data() {
|
||||
return {
|
||||
// 驻店填报 = yxe_self_report,跑店填报 = yxe_visit_report(两张表,分别拉取)
|
||||
typeTabs: [
|
||||
{label: '驻店填报', value: 'store'},
|
||||
{label: '跑店填报', value: 'visit'}
|
||||
],
|
||||
activeType: 'store',
|
||||
statusTabs: [
|
||||
{label: '全部', value: 'all'},
|
||||
{label: '待审核', value: 0},
|
||||
@@ -226,10 +273,10 @@ export default {
|
||||
if (this.activeStatus !== 'all' && Number(item.status) !== Number(this.activeStatus)) {
|
||||
return false
|
||||
}
|
||||
if (this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
|
||||
if (this.filterForm.chainId && !this.matchesItemChain(item)) {
|
||||
return false
|
||||
}
|
||||
if (keyword && !String(item.merchantName || '').toLowerCase().includes(keyword)) {
|
||||
if (keyword && !this.matchesItemKeyword(item, keyword)) {
|
||||
return false
|
||||
}
|
||||
if ((startTs || endTs) && !Number.isFinite(item.submitTimestamp)) {
|
||||
@@ -291,7 +338,8 @@ export default {
|
||||
async fetchList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const {data} = await selfReportUserListReq({})
|
||||
const req = this.activeType === 'visit' ? visitReportUserListReq : selfReportUserListReq
|
||||
const {data} = await req({})
|
||||
this.records = this.unwrapList(data)
|
||||
} catch (error) {
|
||||
this.records = []
|
||||
@@ -321,6 +369,9 @@ export default {
|
||||
}
|
||||
},
|
||||
normalizeRecord(item = {}) {
|
||||
if (this.activeType === 'visit') {
|
||||
return this.normalizeVisitRecord(item)
|
||||
}
|
||||
const createRaw = item.createTime || item.createdAt || item.date || ''
|
||||
const clockPhotos = this.parseMaybeJson(item.clockPhotos, [])
|
||||
const boothPhotos = this.parseMaybeJson(item.boothPhotos, [])
|
||||
@@ -346,7 +397,48 @@ export default {
|
||||
clockImages: this.normalizeMediaList(clockPhotos),
|
||||
boothImages: this.normalizeMediaList(boothPhotos),
|
||||
receiptImages: this.normalizeMediaList(receiptPhoto),
|
||||
salesRows: this.buildSalesRows(salesGroups)
|
||||
salesRows: this.buildSalesRows(salesGroups),
|
||||
storeCount: 0,
|
||||
storeRows: []
|
||||
}
|
||||
},
|
||||
// 跑店填报没有单门店/活动时间/打卡/销量,卡片主体是巡店列表
|
||||
normalizeVisitRecord(item = {}) {
|
||||
const createRaw = item.createTime || item.createdAt || ''
|
||||
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 => ({
|
||||
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) : '',
|
||||
location: store && store.location ? store.location : '',
|
||||
lat: store && store.lat !== undefined ? store.lat : '',
|
||||
lng: store && store.lng !== undefined ? store.lng : '',
|
||||
photos: this.normalizeMediaList(store && store.photos)
|
||||
}))
|
||||
return {
|
||||
id: item.id,
|
||||
merchantId: '',
|
||||
merchantName: '',
|
||||
chainName: '-',
|
||||
chainIdNormalized: '',
|
||||
activityTime: '',
|
||||
date: '',
|
||||
status: item.status,
|
||||
rejectReason: item.rejectReason || item.reject_reason || '',
|
||||
reward: item.reward,
|
||||
rating: '',
|
||||
summary,
|
||||
submitTime: this.formatDateTime(createRaw, createRaw || '-'),
|
||||
submitTimestamp: this.parseDateToTimestamp(createRaw),
|
||||
clockImages: [],
|
||||
boothImages: [],
|
||||
receiptImages: [],
|
||||
salesRows: [],
|
||||
// 门店数与主门店关键词都从列表内容推导,避免列表页再下发冗余字段
|
||||
storeCount: Number(item.storeCount) || storeRows.length,
|
||||
storeRows
|
||||
}
|
||||
},
|
||||
buildSalesRows(groups) {
|
||||
@@ -413,6 +505,21 @@ export default {
|
||||
value
|
||||
}
|
||||
},
|
||||
// 跑店记录没有单一门店/连锁,判定要落到巡店列表的每一行
|
||||
matchesItemChain(item) {
|
||||
const selectedId = this.filterForm.chainId
|
||||
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
|
||||
if (this.activeType === 'visit') {
|
||||
return (item.storeRows || []).some(store => this.matchesSelectedChain('', store.chainName, selectedId))
|
||||
}
|
||||
return this.matchesSelectedChain(item.chainIdNormalized, item.chainName, selectedId)
|
||||
},
|
||||
matchesItemKeyword(item, keyword) {
|
||||
if (this.activeType === 'visit') {
|
||||
return (item.storeRows || []).some(store => String(store.merchantName || '').toLowerCase().includes(keyword))
|
||||
}
|
||||
return String(item.merchantName || '').toLowerCase().includes(keyword)
|
||||
},
|
||||
matchesSelectedChain(chainId, chainName, selectedId) {
|
||||
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
|
||||
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
|
||||
@@ -477,9 +584,33 @@ export default {
|
||||
},
|
||||
handleResubmit(item) {
|
||||
if (!item || item.id === undefined || item.id === null) return
|
||||
uni.navigateTo({
|
||||
url: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
|
||||
})
|
||||
const url = this.activeType === 'visit'
|
||||
? `/packageUser/self-report/visit?id=${item.id}&mode=resubmit`
|
||||
: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
|
||||
uni.navigateTo({url})
|
||||
},
|
||||
cardTitle(item) {
|
||||
if (this.activeType === 'visit') {
|
||||
const names = (item.storeRows || [])
|
||||
.map(store => store.merchantName)
|
||||
.filter(Boolean)
|
||||
if (names.length) {
|
||||
return names.length > 2 ? `${names.slice(0, 2).join('、')} 等${names.length}家` : names.join('、')
|
||||
}
|
||||
return '跑店填报'
|
||||
}
|
||||
return item.merchantName || '-'
|
||||
},
|
||||
cardSubTitle(item) {
|
||||
if (this.activeType === 'visit') return ''
|
||||
return item.chainName && item.chainName !== '-' ? `连锁:${item.chainName}` : ''
|
||||
},
|
||||
changeType(value) {
|
||||
if (this.activeType === value) return
|
||||
this.activeType = value
|
||||
this.expandedMap = {}
|
||||
this.records = []
|
||||
this.fetchList()
|
||||
},
|
||||
changeStatus(value) {
|
||||
this.activeStatus = value
|
||||
@@ -581,6 +712,14 @@ export default {
|
||||
margin: 18rpx 20rpx 0;
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding-bottom: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #eef2f7;
|
||||
}
|
||||
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
@@ -839,6 +978,48 @@ export default {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.store-block {
|
||||
padding: 18rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f9fafb;
|
||||
border: 1rpx solid #eef2f7;
|
||||
}
|
||||
|
||||
.store-block:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.store-block-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.store-block-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.store-block-time {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.store-block-line {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #4b5563;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.store-block .media-images {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.sales-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user