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:
+184
-11
@@ -106,6 +106,16 @@
|
||||
@updated="handleSelfReportUpdated"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="activeType === 'visitReport'">
|
||||
<AuditVisitReportItem
|
||||
v-for="item in currentTasks"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@preview="preview"
|
||||
@approve="handleVisitReportApprove"
|
||||
@reject="handleVisitReportReject"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AuditShareItem
|
||||
v-for="item in currentTasks"
|
||||
@@ -127,21 +137,40 @@
|
||||
<view v-if="showRejectPopup" class="income-popup-mask" @tap="closeRejectPopup"></view>
|
||||
<view v-if="showRejectPopup" class="income-popup card" @tap.stop>
|
||||
<view class="income-popup-head">
|
||||
<view class="section-title">{{ pendingRejectType === 'selfReport' ? '填写驳回原因(可选)' : '填写拒绝原因' }}</view>
|
||||
<view class="section-title">{{ isSelfReportLikeRejectType(pendingRejectType) ? '填写驳回原因(可选)' : '填写拒绝原因' }}</view>
|
||||
<view class="close-btn" @tap="closeRejectPopup">关闭</view>
|
||||
</view>
|
||||
<view class="income-form">
|
||||
<view class="income-label">{{ pendingRejectType === 'selfReport' ? '驳回原因' : '拒绝原因' }}</view>
|
||||
<view class="income-label">{{ isSelfReportLikeRejectType(pendingRejectType) ? '驳回原因' : '拒绝原因' }}</view>
|
||||
<input
|
||||
v-model="rejectReason"
|
||||
class="income-input"
|
||||
type="text"
|
||||
:placeholder="pendingRejectType === 'selfReport' ? '可不填写' : '请输入拒绝原因'"
|
||||
:placeholder="isSelfReportLikeRejectType(pendingRejectType) ? '可不填写' : '请输入拒绝原因'"
|
||||
/>
|
||||
</view>
|
||||
<view class="primary-btn income-submit" @tap="submitRejectReason">提交</view>
|
||||
</view>
|
||||
|
||||
<!-- 跑店填报没有销量小计,奖金只能审核时手填 -->
|
||||
<view v-if="showRewardPopup" class="income-popup-mask" @tap="closeRewardPopup"></view>
|
||||
<view v-if="showRewardPopup" class="income-popup card" @tap.stop>
|
||||
<view class="income-popup-head">
|
||||
<view class="section-title">输入奖金金额</view>
|
||||
<view class="close-btn" @tap="closeRewardPopup">关闭</view>
|
||||
</view>
|
||||
<view class="income-form">
|
||||
<view class="income-label">奖金金额</view>
|
||||
<input
|
||||
v-model="rewardAmount"
|
||||
class="income-input"
|
||||
type="digit"
|
||||
placeholder="请输入奖金金额,可填0"
|
||||
/>
|
||||
</view>
|
||||
<view class="primary-btn income-submit" @tap="submitRewardAmount">提交</view>
|
||||
</view>
|
||||
|
||||
<u-picker
|
||||
:show="showChainPicker"
|
||||
:columns="[chainOptions]"
|
||||
@@ -178,21 +207,24 @@ import {auditUserTaskPageReq, changeUserTaskReq, listUserTaskReq} from "@/api/ta
|
||||
import dayjs from 'dayjs'
|
||||
import {shareAuditPageReq, shareChangeReq, sharePageReq} from "@/api/share";
|
||||
import {selfReportAuditPageReq, selfReportChangeReq, selfReportPageReq} from "@/api/self-report";
|
||||
import {visitReportAuditPageReq, visitReportChangeReq, visitReportPageReq} from "@/api/visit-report";
|
||||
import AppNavbar from "@/components/AppNavbar.vue";
|
||||
import AuditMovementItem from "./components/AuditMovementItem.vue";
|
||||
import AuditStoreVisitItem from "./components/AuditStoreVisitItem.vue";
|
||||
import AuditShareItem from "./components/AuditShareItem.vue";
|
||||
import AuditSelfReportItem from "./components/AuditSelfReportItem.vue";
|
||||
import AuditVisitReportItem from "./components/AuditVisitReportItem.vue";
|
||||
import {dictDataListByDictReq} from "@/api/common";
|
||||
|
||||
export default {
|
||||
components: {AppNavbar, AuditMovementItem, AuditStoreVisitItem, AuditShareItem, AuditSelfReportItem},
|
||||
components: {AppNavbar, AuditMovementItem, AuditStoreVisitItem, AuditShareItem, AuditSelfReportItem, AuditVisitReportItem},
|
||||
data() {
|
||||
return {
|
||||
typeTabs: [
|
||||
{label: '药动销', value: 'movement'},
|
||||
{label: '跑店/巡店', value: 'storeVisit'},
|
||||
{label: '自主填报', value: 'selfReport'},
|
||||
{label: '驻店填报', value: 'selfReport'},
|
||||
{label: '跑店填报', value: 'visitReport'},
|
||||
{label: '药分享', value: 'exchange'}
|
||||
],
|
||||
statusTabs: [
|
||||
@@ -206,6 +238,7 @@ export default {
|
||||
movementTasks: [],
|
||||
storeVisitTasks: [],
|
||||
selfReportTasks: [],
|
||||
visitReportTasks: [],
|
||||
exchangeTasks: [],
|
||||
loading: false,
|
||||
chainLoading: false,
|
||||
@@ -225,6 +258,10 @@ export default {
|
||||
rejectReason: '',
|
||||
pendingReject: null,
|
||||
pendingRejectType: 'audit',
|
||||
// 跑店填报审核通过时的奖金输入
|
||||
showRewardPopup: false,
|
||||
rewardAmount: '',
|
||||
pendingRewardItem: null,
|
||||
pageNo: 1,
|
||||
pageSize: 5,
|
||||
hasMore: true,
|
||||
@@ -236,10 +273,12 @@ export default {
|
||||
if (this.activeType === 'movement') return this.movementTasks
|
||||
if (this.activeType === 'storeVisit') return this.storeVisitTasks
|
||||
if (this.activeType === 'selfReport') return this.selfReportTasks
|
||||
if (this.activeType === 'visitReport') return this.visitReportTasks
|
||||
return this.exchangeTasks
|
||||
},
|
||||
displayStatusTabs() {
|
||||
if (this.activeType === 'selfReport') {
|
||||
// 驻店/跑店填报都由审核端驳回,没有「抄送」态
|
||||
if (this.isSelfReportLikeType) {
|
||||
return this.statusTabs.filter(item => item.value !== 3)
|
||||
}
|
||||
return this.statusTabs
|
||||
@@ -247,6 +286,13 @@ export default {
|
||||
isStoreVisitType() {
|
||||
return this.activeType === 'storeVisit'
|
||||
},
|
||||
// 跑店填报的连锁在 store_list JSON 里,没有 chain_id 可匹配,只能按连锁名模糊查
|
||||
useChainNameFilter() {
|
||||
return this.activeType === 'visitReport'
|
||||
},
|
||||
isSelfReportLikeType() {
|
||||
return this.activeType === 'selfReport' || this.activeType === 'visitReport'
|
||||
},
|
||||
chainLabel() {
|
||||
const selected = this.chainOptions.find(item => item.id === this.filterForm.chainId)
|
||||
return (selected && selected.text) || '连锁筛选'
|
||||
@@ -274,12 +320,13 @@ export default {
|
||||
await this.fetchList()
|
||||
},
|
||||
getPageSize(type = this.activeType) {
|
||||
return type === 'selfReport' ? 10 : this.pageSize
|
||||
return this.isSelfReportLikeType || type === 'selfReport' || type === 'visitReport' ? 10 : this.pageSize
|
||||
},
|
||||
getTaskListByType(type = this.activeType) {
|
||||
if (type === 'movement') return this.movementTasks
|
||||
if (type === 'storeVisit') return this.storeVisitTasks
|
||||
if (type === 'selfReport') return this.selfReportTasks
|
||||
if (type === 'visitReport') return this.visitReportTasks
|
||||
return this.exchangeTasks
|
||||
},
|
||||
setTaskListByType(type = this.activeType, list = []) {
|
||||
@@ -295,6 +342,10 @@ export default {
|
||||
this.selfReportTasks = list
|
||||
return
|
||||
}
|
||||
if (type === 'visitReport') {
|
||||
this.visitReportTasks = list
|
||||
return
|
||||
}
|
||||
this.exchangeTasks = list
|
||||
},
|
||||
resetPagination() {
|
||||
@@ -450,6 +501,28 @@ export default {
|
||||
}
|
||||
return params
|
||||
},
|
||||
// 跑店填报表里没有 chain_id/merchant_id,只能按 store_list 文本模糊匹配
|
||||
buildVisitReportFilterParams() {
|
||||
const params = {}
|
||||
const userGroup = (this.filterForm.userGroup || '').trim()
|
||||
if (userGroup) params.userGroup = userGroup
|
||||
const keyword = (this.filterForm.merchantKeyword || '').trim()
|
||||
const chainText = this.selectedChainText()
|
||||
const startDate = this.filterForm.startDate
|
||||
const endDate = this.filterForm.endDate
|
||||
if (chainText) params.chainName = chainText
|
||||
if (keyword) params.merchantName = keyword
|
||||
if (startDate) params.dateStart = startDate
|
||||
if (endDate) params.dateEnd = endDate
|
||||
return params
|
||||
},
|
||||
// 下拉里选的是连锁的 id,新表按名字查,需要回查成文本
|
||||
selectedChainText() {
|
||||
const selectedId = this.filterForm.chainId
|
||||
if (selectedId === '' || selectedId === null || selectedId === undefined) return ''
|
||||
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
|
||||
return option ? (option.text || '') : ''
|
||||
},
|
||||
filterExchangeList(list = []) {
|
||||
const keyword = (this.filterForm.merchantKeyword || '').trim().toLowerCase()
|
||||
const startTs = this.filterForm.startDate ? dayjs(`${this.filterForm.startDate} 00:00:00`).valueOf() : null
|
||||
@@ -582,7 +655,15 @@ export default {
|
||||
this.filterForm.chainId = ''
|
||||
this.filterForm.merchantKeyword = ''
|
||||
}
|
||||
if (value === 'selfReport' && this.activeStatus === 3) {
|
||||
// 跑店填报没有 chain_id 列,连锁只能在已加载的选项里按名字查;
|
||||
// 切到该类型时若选中的是虚拟连锁(chain_ 前缀),清掉避免查出空列表
|
||||
if (value === 'visitReport') {
|
||||
const chainIdText = String(this.getSelectedChainApiValue(this.filterForm.chainId) || '')
|
||||
if (!chainIdText || chainIdText.startsWith('chain_')) {
|
||||
this.filterForm.chainId = ''
|
||||
}
|
||||
}
|
||||
if ((value === 'selfReport' || value === 'visitReport') && this.activeStatus === 3) {
|
||||
this.activeStatus = 0
|
||||
}
|
||||
this.activeType = value
|
||||
@@ -672,6 +753,30 @@ export default {
|
||||
this.setTaskListByType('selfReport', merged)
|
||||
this.updatePagination(pageResult, requestPage, list.length, pageSize)
|
||||
}
|
||||
} else if (this.activeType === 'visitReport') {
|
||||
if (isCopyQuery) {
|
||||
this.setTaskListByType('visitReport', [])
|
||||
this.hasMore = false
|
||||
} else {
|
||||
const params = {
|
||||
status: this.activeStatus,
|
||||
page: requestPage,
|
||||
limit: pageSize,
|
||||
auditLite: 1
|
||||
}
|
||||
Object.assign(params, this.buildVisitReportFilterParams())
|
||||
const {data} = await visitReportAuditPageReq(params)
|
||||
const pageResult = this.extractPageResult(data)
|
||||
const list = pageResult.list
|
||||
const normalized = list.map(item => this.normalizeVisitReport(item))
|
||||
const nextList = this.filterMovementList(normalized, {
|
||||
enableChainFilter: false,
|
||||
enableMerchantFilter: false
|
||||
})
|
||||
const merged = append ? this.getTaskListByType('visitReport').concat(nextList) : nextList
|
||||
this.setTaskListByType('visitReport', merged)
|
||||
this.updatePagination(pageResult, requestPage, list.length, pageSize)
|
||||
}
|
||||
} else {
|
||||
const params = {
|
||||
status: isCopyQuery ? 2 : this.activeStatus,
|
||||
@@ -798,6 +903,27 @@ export default {
|
||||
submitTimestamp: this.parseDateToTimestamp(activityDate)
|
||||
}
|
||||
},
|
||||
// 跑店填报:卡片主体是巡店列表,列表接口不带 storeList(大字段),展开时再拉详情
|
||||
normalizeVisitReport(item) {
|
||||
const createRaw = item.createTime || item.createdAt || ''
|
||||
const summaryRaw = this.parseJson(item.summary, '')
|
||||
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw)
|
||||
const user = item && item.user ? item.user : {}
|
||||
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
|
||||
return {
|
||||
id: item.id,
|
||||
userId: item.userId,
|
||||
storeCount: Number(item.storeCount) || 0,
|
||||
storeList: item.storeList || '',
|
||||
owner,
|
||||
status: item.status,
|
||||
reward: item.reward,
|
||||
location: item.location || '',
|
||||
summary,
|
||||
rejectReason: item.rejectReason || '',
|
||||
submitTimestamp: this.parseDateToTimestamp(createRaw)
|
||||
}
|
||||
},
|
||||
formatAmount(value) {
|
||||
const num = Number(value || 0)
|
||||
return num.toFixed(2)
|
||||
@@ -895,6 +1021,48 @@ export default {
|
||||
async handleSelfReportUpdated() {
|
||||
await this.reloadFromFirstPage()
|
||||
},
|
||||
// 跑店填报与驻店填报共用同一套审核动作,仅接口不同。
|
||||
// 跑店没有销量小计,奖金得审核端手填,所以先弹输入框。
|
||||
async handleVisitReportApprove(item) {
|
||||
this.pendingRewardItem = item
|
||||
this.rewardAmount = item.reward === null || item.reward === undefined ? '' : `${item.reward}`
|
||||
this.showRewardPopup = true
|
||||
},
|
||||
closeRewardPopup() {
|
||||
this.showRewardPopup = false
|
||||
this.rewardAmount = ''
|
||||
this.pendingRewardItem = null
|
||||
},
|
||||
async submitRewardAmount() {
|
||||
const item = this.pendingRewardItem
|
||||
if (!item) {
|
||||
this.closeRewardPopup()
|
||||
return
|
||||
}
|
||||
const raw = String(this.rewardAmount === null || this.rewardAmount === undefined ? '' : this.rewardAmount).trim()
|
||||
if (!raw) {
|
||||
uni.showToast({title: '请输入奖金金额', icon: 'none'})
|
||||
return
|
||||
}
|
||||
const reward = Number(raw)
|
||||
if (!Number.isFinite(reward) || reward < 0) {
|
||||
uni.showToast({title: '奖金金额不能小于0', icon: 'none'})
|
||||
return
|
||||
}
|
||||
await visitReportChangeReq({
|
||||
id: item.id,
|
||||
status: 1,
|
||||
reward
|
||||
})
|
||||
uni.showToast({
|
||||
title: '操作成功'
|
||||
})
|
||||
this.closeRewardPopup()
|
||||
await this.reloadFromFirstPage()
|
||||
},
|
||||
async handleVisitReportReject(item) {
|
||||
this.openRejectPopup(item, 'visitReport')
|
||||
},
|
||||
async handleShareUpdate({item, payload, localDetailList}) {
|
||||
const data = this.buildSharePayload(item, payload)
|
||||
await shareChangeReq(data)
|
||||
@@ -928,7 +1096,7 @@ export default {
|
||||
},
|
||||
async submitRejectReason() {
|
||||
const reason = (this.rejectReason || '').trim()
|
||||
if (this.pendingRejectType !== 'selfReport' && !reason) {
|
||||
if (!this.isSelfReportLikeRejectType(this.pendingRejectType) && !reason) {
|
||||
uni.showToast({
|
||||
title: '请输入拒绝原因',
|
||||
icon: 'none'
|
||||
@@ -939,8 +1107,9 @@ export default {
|
||||
this.closeRejectPopup()
|
||||
return
|
||||
}
|
||||
if (this.pendingRejectType === 'selfReport') {
|
||||
await selfReportChangeReq({
|
||||
if (this.isSelfReportLikeRejectType(this.pendingRejectType)) {
|
||||
const req = this.pendingRejectType === 'visitReport' ? visitReportChangeReq : selfReportChangeReq
|
||||
await req({
|
||||
id: this.pendingReject.id,
|
||||
status: 2,
|
||||
rejectReason: reason
|
||||
@@ -955,6 +1124,10 @@ export default {
|
||||
await this.submitAuditAction(3, this.pendingReject, reason)
|
||||
this.closeRejectPopup()
|
||||
},
|
||||
// 驻店/跑店填报的驳回都是「驳回原因可不填」
|
||||
isSelfReportLikeRejectType(type) {
|
||||
return type === 'selfReport' || type === 'visitReport'
|
||||
},
|
||||
preview(url, list) {
|
||||
uni.previewImage({
|
||||
current: url,
|
||||
|
||||
Reference in New Issue
Block a user