feat(填报记录): 被驳回记录支持修改后重新提交

- records.vue:已驳回记录卡片底部新增“重新提交”按钮,跳转
  create?id=<id>&mode=resubmit;同时补上驳回原因展示
  (原 normalizeRecord 未提取 rejectReason,列表页看不到驳回理由)
- create.vue:复用同一表单页做编辑模式,onLoad 读 id/mode,
  回填门店、日期、打卡照、摆台照、小票、销量、小结、评分、定位;
  标题与按钮切换为“重新提交填报/重新提交”,隐藏保存草稿
  (草稿 key 与新增共用,会互相覆盖)
- 提交走 resubmit 接口,成功后返回列表,onShow 自动刷新为待审核
- api/self-report.js:新增 selfReportResubmitReq
This commit is contained in:
2026-10-11 17:05:12 +08:00
parent 4556114b4d
commit 9eaa168d72
3 changed files with 234 additions and 9 deletions
+3
View File
@@ -16,4 +16,7 @@ export const selfReportInfoReq = id => get(`/yxe/yxe-self-report/${id}`)
export const selfReportChangeReq = data => put('/yxe/yxe-self-report', data)
// 驳回后重新提交:仅本人、仅已驳回记录可调用,状态由服务端置回待审核
export const selfReportResubmitReq = data => put('/yxe/yxe-self-report/resubmit', data)
export const selfReportDeleteReq = id => del(`/yxe/yxe-self-report/${id}`)
+183 -9
View File
@@ -1,6 +1,6 @@
<template>
<view class="self-report-page">
<AppNavbar title="自主填报" showBack/>
<AppNavbar :title="isResubmit ? '重新提交填报' : '自主填报'" showBack/>
<view class="sr-merchant-row" @tap="openChainPicker">
<view class="sr-merchant-label">连锁筛选</view>
@@ -185,9 +185,14 @@
</view>
</view>
<view class="card block" v-if="isResubmit && rejectReason">
<view class="section-title">驳回原因</view>
<view class="reject-reason-text">{{ rejectReason }}</view>
</view>
<view class="form-actions">
<view class="draft-btn" @tap="saveDraft">保存草稿</view>
<view class="primary-btn submit" @tap="submit">{{ submitLoading ? '提交中...' : '提交销量' }}</view>
<view v-if="!isResubmit" class="draft-btn" @tap="saveDraft">保存草稿</view>
<view class="primary-btn submit" @tap="submit">{{ submitLoading ? '提交中...' : (isResubmit ? '重新提交' : '提交销量') }}</view>
</view>
<view class="form-actions-placeholder"/>
@@ -322,7 +327,7 @@ import Login from "@/components/Login.vue";
import AppNavbar from "@/components/AppNavbar.vue";
import {merchantGroupByChainReq} from "@/api/shop";
import {productGroupByCateReq} from "@/api/product";
import {selfReportAddReq} from "@/api/self-report";
import {selfReportAddReq, selfReportInfoReq, selfReportResubmitReq} from "@/api/self-report";
import {getUserInfo} from "@/util/user";
import {chooseImg, getLocationOnly} from "@/util";
@@ -362,7 +367,12 @@ export default {
currentProvince: '',
currentCity: '',
draftPrompting: false,
submitLoading: false
submitLoading: false,
// 驳回后重新提交:记录 id 与驳回原因,提交时走 resubmit 接口
isResubmit: false,
resubmitId: '',
rejectReason: '',
resubmitLoading: false
}
},
computed: {
@@ -449,12 +459,23 @@ export default {
// 门店明细挂实例上而非 data:data 里的一切都会进页面 data 树并被 setData 序列化。
this.merchantStore = []
},
async onLoad(options = {}) {
const id = options && options.id !== undefined && options.id !== null ? String(options.id).trim() : ''
if (id && String(options.mode || '') === 'resubmit') {
this.isResubmit = true
this.resubmitId = id
}
},
async onShow() {
this.isLoggedIn = !!getUserInfo().token
if (!this.hasInitialized) {
await this.init()
this.hasInitialized = true
await this.checkDraftOnEntry()
if (this.isResubmit) {
await this.loadResubmitRecord()
} else {
await this.checkDraftOnEntry()
}
}
},
methods: {
@@ -501,6 +522,138 @@ export default {
const uid = user.uid || user.id || user.userId || 'guest'
return `yxe_self_report_create_draft_${uid}`
},
parseStoredJson(value, fallback) {
if (value === null || value === undefined || value === '') return fallback
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch (error) {
return fallback
}
}
return value
},
// 记录里的图片字段可能是字符串/对象/数组混用,统一取 URL
pickMediaUrl(media) {
if (!media) return ''
if (typeof media === 'string') return media
return media.url || media.path || media.image || ''
},
normalizeResubmitClockPhotos(value) {
const list = this.parseStoredJson(value, [])
const source = Array.isArray(list) ? list : []
const defaults = [
{key: 'in', label: '上班'},
{key: 'out', label: '下班'}
]
return defaults.map((item, index) => {
const raw = source[index] && typeof source[index] === 'object' ? source[index] : {}
return {
key: raw.key || item.key,
label: raw.label || item.label,
time: raw.time || '',
date: raw.date || '',
url: this.pickMediaUrl(raw.url || raw) || '',
lat: raw.lat || '',
lng: raw.lng || '',
location: raw.location || ''
}
})
},
normalizeResubmitPhotos(value) {
const list = this.parseStoredJson(value, [])
return (Array.isArray(list) ? list : []).map(item => this.pickMediaUrl(item)).filter(Boolean)
},
// 销量条目回填成 create 页的 salesItems 结构,specOptions 缺失时用已有 spec 兜底
normalizeResubmitSalesItems(value) {
const groups = this.parseStoredJson(value, [])
const items = []
;(Array.isArray(groups) ? groups : []).forEach((group, groupIndex) => {
const rows = Array.isArray(group && group.items) ? group.items : []
rows.forEach((detail, rowIndex) => {
if (!detail || typeof detail !== 'object') return
const spec = detail.spec || detail.specification || ''
const specOptions = Array.isArray(detail.specOptions) && detail.specOptions.length
? detail.specOptions.map(item => `${item}`.trim()).filter(Boolean)
: (spec ? [spec] : [])
items.push({
_key: `resubmit_${groupIndex}_${rowIndex}`,
productId: detail.productId || detail.goodsId || '',
goodsCode: detail.goodsCode || detail.code || '',
name: detail.name || detail.goodsName || '',
cover: detail.cover || '',
makerAbb: detail.makerAbb || detail.manufacturer || '',
groupTitle: (group && group.title) || detail.groupTitle || '自定义商品',
spec: spec || specOptions[0] || '',
specOptions,
unit: detail.unit || '盒',
taskIncome: detail.taskIncome !== undefined && detail.taskIncome !== null ? detail.taskIncome : '',
qty: detail.qty !== undefined && detail.qty !== null ? `${detail.qty}` : ''
})
})
})
return items
},
async loadResubmitRecord() {
if (!this.resubmitId || this.resubmitLoading) return
this.resubmitLoading = true
try {
const res = await selfReportInfoReq(this.resubmitId)
// get 请求在业务失败时也会 resolve,需要显式看 code
if (res && res.code !== 0) {
await this.toast(res.message || '填报记录加载失败')
uni.navigateBack()
return
}
const record = res && res.data && typeof res.data === 'object' ? res.data : null
if (!record) {
await this.toast('填报记录不存在')
uni.navigateBack()
return
}
if (Number(record.status) !== 2) {
await this.toast('该记录不是已驳回状态,无法重新提交')
uni.navigateBack()
return
}
this.rejectReason = record.rejectReason || record.reject_reason || ''
this.form = Object.assign(this.buildForm(), {
chainId: record.chainId || record.chain_id || '',
chainName: record.chainName || record.chain_name || '',
merchantId: record.merchantId || record.merchant_id || '',
merchantName: record.merchantName || record.merchant_name || '',
date: record.date || '',
province: record.province || '',
city: record.city || '',
summary: this.parseStoredJson(record.summary, record.summary || ''),
rating: Number(record.rating) || 5
})
if (typeof this.form.summary !== 'string') {
this.form.summary = ''
}
this.clockPhotos = this.normalizeResubmitClockPhotos(record.clockPhotos)
this.boothPhotos = this.normalizeResubmitPhotos(record.boothPhotos)
this.receiptPhotos = this.normalizeResubmitPhotos(record.receiptPhoto || record.receiptPhotos)
this.salesItems = this.normalizeResubmitSalesItems(record.salesGroups)
this.currentAddress = record.location || ''
this.lat = record.lat === '' || record.lat === null || record.lat === undefined ? null : Number(record.lat)
this.lng = record.lng === '' || record.lng === null || record.lng === undefined ? null : Number(record.lng)
this.currentProvince = record.province || ''
this.currentCity = record.city || ''
// 原记录没有定位时用当前位置兜底,否则校验会卡在“请先完成定位”且用户无从修改
if (!this.currentAddress || !this.lat || !this.lng) {
await this.fetchCurrentLocation()
}
// 销量商品按连锁重新拉取,保证规格选项与当前连锁一致
if (this.form.chainName) {
await this.getProductGroups()
}
} catch (error) {
await this.toast(error && error.message ? error.message : '填报记录加载失败')
} finally {
this.resubmitLoading = false
}
},
cloneValue(value) {
if (value === undefined || value === null) {
return value
@@ -1437,9 +1590,18 @@ export default {
if (!this.validateSubmit()) return
this.submitLoading = true
try {
await selfReportAddReq(this.buildPayload())
this.clearDraft()
await this.toast('提交成功,请等待审核')
if (this.isResubmit) {
const payload = this.buildPayload()
// 状态与驳回原因由服务端决定,这里不传,避免被当成审核端改状态
delete payload.status
payload.id = this.resubmitId
await selfReportResubmitReq(payload)
await this.toast('重新提交成功,请等待审核')
} else {
await selfReportAddReq(this.buildPayload())
this.clearDraft()
await this.toast('提交成功,请等待审核')
}
uni.navigateBack({
fail: () => {
uni.switchTab({url: '/pages/mine/index'})
@@ -1592,6 +1754,18 @@ export default {
margin-bottom: 0;
}
.reject-reason-text {
margin-top: 14rpx;
padding: 18rpx 20rpx;
border-radius: 16rpx;
background: #fef2f2;
border: 1rpx solid #fecaca;
color: #dc2626;
font-size: 26rpx;
line-height: 1.6;
word-break: break-word;
}
.section-sub {
margin-left: 8rpx;
font-size: 24rpx;
+48
View File
@@ -69,6 +69,9 @@
</view>
</view>
<view class="summary-line">小结:{{ item.summary || '-' }}</view>
<view v-if="isRejected(item.status) && item.rejectReason" class="reject-line">
驳回原因:{{ item.rejectReason }}
</view>
<view class="record-toggle" @tap="toggleExpanded(item.id)">
<text>{{ isExpanded(item.id) ? '收起详情' : '展开详情' }}</text>
<u-icon :name="isExpanded(item.id) ? 'arrow-up' : 'arrow-down'" size="20" color="#2fc6a7"/>
@@ -137,6 +140,10 @@
<view v-else class="detail-empty">暂无销量数据</view>
</view>
</view>
<view v-if="isRejected(item.status)" class="record-actions">
<view class="resubmit-btn" @tap="handleResubmit(item)">重新提交</view>
</view>
</view>
</view>
</view>
@@ -330,6 +337,7 @@ export default {
activityTime: item.activityTime || item.activity_time || '',
date: item.date || '',
status: item.status,
rejectReason: item.rejectReason || item.reject_reason || '',
reward: item.reward,
rating: item.rating,
summary,
@@ -464,6 +472,15 @@ export default {
if (Number(status) === 2) return 'danger'
return 'pending'
},
isRejected(status) {
return Number(status) === 2
},
handleResubmit(item) {
if (!item || item.id === undefined || item.id === null) return
uni.navigateTo({
url: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
})
},
changeStatus(value) {
this.activeStatus = value
},
@@ -743,6 +760,37 @@ export default {
word-break: break-word;
}
.reject-line {
margin-top: 10rpx;
color: #dc2626;
font-size: 24rpx;
line-height: 1.5;
word-break: break-word;
}
.record-actions {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #eef2f7;
display: flex;
justify-content: flex-end;
}
.resubmit-btn {
min-width: 200rpx;
min-height: 72rpx;
padding: 0 32rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #2fc6a7, #1db896);
color: #ffffff;
font-size: 26rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 20rpx rgba(47, 198, 167, 0.22);
}
.record-toggle {
margin-top: 16rpx;
display: inline-flex;