Files
yxe-mp/pages/audit/index.vue
T
2026-09-29 15:17:13 +08:00

1388 lines
46 KiB
Vue

<template>
<view class="audit-page">
<view class="page-hero">
<AppNavbar title="药审核" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>药审核</text>
</view>
</view>
</view>
<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 displayStatusTabs"
:key="tab.value"
:class="['tab', activeStatus === tab.value ? 'active' : '']"
@tap="changeStatus(tab.value)"
>
{{ tab.label }}
</view>
</view>
</view>
<view class="card block">
<view class="section-title">筛选查询</view>
<view class="filter-row">
<view v-if="!isStoreVisitType" class="filter-chip" @tap="openChainPicker">{{ chainLabel }}</view>
<input v-model="filterForm.userGroup" class="group-input" type="text" confirm-type="search" placeholder="用户分组" placeholder-class="keyword-placeholder" @confirm="handleQuery" />
<view class="filter-chip" @tap="openDatePicker('start')">{{ filterForm.startDate || '开始日期' }}</view>
<view class="filter-chip" @tap="openDatePicker('end')">{{ filterForm.endDate || '结束日期' }}</view>
</view>
<view class="keyword-row">
<input
v-if="!isStoreVisitType"
v-model="filterForm.merchantKeyword"
class="keyword-input"
type="text"
confirm-type="search"
placeholder="请输入门店关键词"
placeholder-class="keyword-placeholder"
@confirm="handleQuery"
/>
<view class="filter-btn primary" @tap="handleQuery">查询</view>
<view class="filter-btn" @tap="handleReset">重置</view>
</view>
</view>
<!-- <view class="card block">-->
<!-- <view class="section-title">快捷操作</view>-->
<!-- <view class="action-row">-->
<!-- <view class="primary-btn action-btn" @tap="handleAction('审核')">审核</view>-->
<!-- <view class="primary-btn action-btn" @tap="handleAction('抄送')">抄送</view>-->
<!-- <view class="warn-btn action-btn" @tap="handleAction('驳回')">驳回</view>-->
<!-- <view class="danger-btn action-btn" @tap="handleAction('删除')">删除</view>-->
<!-- </view>-->
<!-- <view class="tip">点击任意按钮会立即给出处理反馈。</view>-->
<!-- </view>-->
<view class="block">
<view class="section-title">记录列表</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="currentTasks.length === 0" class="empty">暂无记录</view>
<view v-else>
<template v-if="activeType === 'movement'">
<AuditMovementItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleAction(2, $event)"
@reject="handleAction(3, $event)"
@copied="fetchList"
/>
</template>
<template v-else-if="activeType === 'storeVisit'">
<AuditStoreVisitItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleAction(2, $event)"
@reject="handleAction(3, $event)"
@copied="fetchList"
/>
</template>
<template v-else-if="activeType === 'selfReport'">
<AuditSelfReportItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleSelfReportApprove"
@reject="handleSelfReportReject"
@updated="handleSelfReportUpdated"
/>
</template>
<template v-else>
<AuditShareItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@approve="handleShareApprove"
@reject="handleActionShare(2, $event)"
@update="handleShareUpdate"
@preview="preview"
@copied="fetchList"
/>
</template>
<view class="load-more-tip">
{{ loadingMore ? '加载更多中...' : (hasMore ? '上滑加载更多' : '没有更多了') }}
</view>
</view>
</view>
<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="close-btn" @tap="closeRejectPopup">关闭</view>
</view>
<view class="income-form">
<view class="income-label">{{ pendingRejectType === 'selfReport' ? '驳回原因' : '拒绝原因' }}</view>
<input
v-model="rejectReason"
class="income-input"
type="text"
:placeholder="pendingRejectType === 'selfReport' ? '可不填写' : '请输入拒绝原因'"
/>
</view>
<view class="primary-btn income-submit" @tap="submitRejectReason">提交</view>
</view>
<u-picker
:show="showChainPicker"
:columns="[chainOptions]"
:loading="chainLoading"
:defaultIndex="chainPickerIndex"
keyName="text"
@confirm="onChainConfirm"
@cancel="closeChainPicker"
@close="closeChainPicker"
/>
<u-datetime-picker
:show="showStartPicker"
:value="startPickerValue"
mode="date"
@change="e => onDateChange('start', e)"
@confirm="e => onDateConfirm('start', e)"
@cancel="closeDatePicker('start')"
@close="closeDatePicker('start')"
/>
<u-datetime-picker
:show="showEndPicker"
:value="endPickerValue"
mode="date"
@change="e => onDateChange('end', e)"
@confirm="e => onDateConfirm('end', e)"
@cancel="closeDatePicker('end')"
@close="closeDatePicker('end')"
/>
</view>
</template>
<script>
import {auditUserTaskPageReq, changeUserTaskReq, listUserTaskReq} from "@/api/task";
import dayjs from 'dayjs'
import {shareAuditPageReq, shareChangeReq, sharePageReq} from "@/api/share";
import {selfReportAuditPageReq, selfReportChangeReq, selfReportPageReq} from "@/api/self-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 {dictDataListByDictReq} from "@/api/common";
export default {
components: {AppNavbar, AuditMovementItem, AuditStoreVisitItem, AuditShareItem, AuditSelfReportItem},
data() {
return {
typeTabs: [
{label: '药动销', value: 'movement'},
{label: '跑店/巡店', value: 'storeVisit'},
{label: '自主填报', value: 'selfReport'},
{label: '药分享', value: 'exchange'}
],
statusTabs: [
{label: '待审核', value: 0},
{label: '已通过', value: 1},
{label: '已驳回', value: 2},
{label: '抄送', value: 3}
],
activeType: 'movement',
activeStatus: 0,
movementTasks: [],
storeVisitTasks: [],
selfReportTasks: [],
exchangeTasks: [],
loading: false,
chainLoading: false,
showChainPicker: false,
showStartPicker: false,
showEndPicker: false,
chainOptions: [{id: '', text: '全部连锁'}],
chainMerchantMap: {},
filterForm: {
chainId: '',
userGroup: '',
merchantKeyword: '',
startDate: '',
endDate: ''
},
showRejectPopup: false,
rejectReason: '',
pendingReject: null,
pendingRejectType: 'audit',
pageNo: 1,
pageSize: 5,
hasMore: true,
loadingMore: false
}
},
computed: {
currentTasks() {
if (this.activeType === 'movement') return this.movementTasks
if (this.activeType === 'storeVisit') return this.storeVisitTasks
if (this.activeType === 'selfReport') return this.selfReportTasks
return this.exchangeTasks
},
displayStatusTabs() {
if (this.activeType === 'selfReport') {
return this.statusTabs.filter(item => item.value !== 3)
}
return this.statusTabs
},
isStoreVisitType() {
return this.activeType === 'storeVisit'
},
chainLabel() {
const selected = this.chainOptions.find(item => item.id === this.filterForm.chainId)
return (selected && selected.text) || '连锁筛选'
},
chainPickerIndex() {
const idx = this.chainOptions.findIndex(item => item.id === this.filterForm.chainId)
return [idx > -1 ? idx : 0]
},
startPickerValue() {
return this.filterForm.startDate ? dayjs(this.filterForm.startDate).valueOf() : Date.now()
},
endPickerValue() {
return this.filterForm.endDate ? dayjs(this.filterForm.endDate).valueOf() : Date.now()
}
},
onShow() {
this.initPage()
},
onReachBottom() {
this.loadMore()
},
methods: {
async initPage() {
await this.fetchChainList()
await this.fetchList()
},
getPageSize(type = this.activeType) {
return type === 'selfReport' ? 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
return this.exchangeTasks
},
setTaskListByType(type = this.activeType, list = []) {
if (type === 'movement') {
this.movementTasks = list
return
}
if (type === 'storeVisit') {
this.storeVisitTasks = list
return
}
if (type === 'selfReport') {
this.selfReportTasks = list
return
}
this.exchangeTasks = list
},
resetPagination() {
this.pageNo = 1
this.hasMore = true
},
async loadMore() {
if (this.loading || this.loadingMore || !this.hasMore) return
await this.fetchList({append: true})
},
buildUserTaskFilterParams(options = {}) {
const enableChainFilter = options.enableChainFilter !== false
const enableMerchantFilter = options.enableMerchantFilter !== false
const params = {
page: options.page || this.pageNo,
limit: this.pageSize
}
const chainId = this.getSelectedChainApiValue(this.filterForm.chainId)
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (enableChainFilter && chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (enableMerchantFilter && keyword) {
params.merchantName = keyword
params.keyword = keyword
}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
if (startDate) {
params.createTimeStart = `${startDate} 00:00:00`
params.dateStart = startDate
}
if (endDate) {
params.createTimeEnd = `${endDate} 23:59:59`
params.dateEnd = endDate
}
return params
},
extractPageResult(data) {
if (Array.isArray(data)) {
return {
list: data,
total: data.length,
pages: 1,
current: 1,
size: data.length
}
}
const safeData = data && typeof data === 'object' ? data : {}
const list = Array.isArray(safeData.records)
? safeData.records
: (Array.isArray(safeData.list) ? safeData.list : [])
const total = Number(safeData.total ?? safeData.count)
const pages = Number(safeData.pages)
const current = Number(safeData.current ?? safeData.page ?? safeData.pageNum)
const size = Number(safeData.size ?? safeData.limit ?? safeData.pageSize)
return {
list,
total: Number.isFinite(total) ? total : null,
pages: Number.isFinite(pages) ? pages : null,
current: Number.isFinite(current) ? current : null,
size: Number.isFinite(size) ? size : this.getPageSize()
}
},
updatePagination(pageResult, requestPage, loadedCount, pageSize = this.getPageSize()) {
this.pageNo = requestPage
if (Number.isFinite(pageResult.total) && pageResult.total !== null) {
this.hasMore = requestPage * pageSize < pageResult.total
return
}
if (Number.isFinite(pageResult.pages) && pageResult.pages !== null) {
this.hasMore = requestPage < pageResult.pages
return
}
this.hasMore = loadedCount >= pageSize
},
async fetchChainList() {
this.chainLoading = true
try {
const {data} = await dictDataListByDictReq({
dictId: 175
})
const list = Array.isArray(data) ? data : this.unwrapList(data)
const chainOptions = [{id: '', text: '全部连锁'}]
list
.map((item, index) => this.normalizeChainDictOption(item, index))
.filter(item => item.id !== '' && item.id !== null && item.id !== undefined)
.forEach(item => {
chainOptions.push(item)
})
this.chainOptions = chainOptions
this.chainMerchantMap = {}
} catch (error) {
this.chainOptions = [{id: '', text: '全部连锁'}]
this.chainMerchantMap = {}
} finally {
this.chainLoading = false
}
},
buildShareFilterParams() {
const params = {}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
const chainId = this.getSelectedChainApiValue(this.filterForm.chainId)
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (keyword) {
params.merchantName = keyword
params.keyword = keyword
}
if (startDate) {
params.createTimeStart = `${startDate} 00:00:00`
params.dateStart = startDate
}
if (endDate) {
params.createTimeEnd = `${endDate} 23:59:59`
params.dateEnd = endDate
}
return params
},
buildSelfReportFilterParams() {
const params = {}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
const chainId = this.getSelectedChainApiValue(this.filterForm.chainId)
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (keyword) {
params.merchantName = keyword
params.keywords = keyword
}
if (startDate) {
params.dateStart = startDate
}
if (endDate) {
params.dateEnd = endDate
}
return params
},
filterExchangeList(list = []) {
const keyword = (this.filterForm.merchantKeyword || '').trim().toLowerCase()
const startTs = this.filterForm.startDate ? dayjs(`${this.filterForm.startDate} 00:00:00`).valueOf() : null
const endTs = this.filterForm.endDate ? dayjs(`${this.filterForm.endDate} 23:59:59`).valueOf() : null
return list.filter(item => {
if (this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
return false
}
if (keyword) {
const merchantText = String(item.merchantName || item.merchant_name || '').toLowerCase()
if (!merchantText.includes(keyword)) return false
}
if ((startTs || endTs) && !Number.isFinite(item.activityTimestamp)) {
return false
}
if (startTs && item.activityTimestamp < startTs) {
return false
}
if (endTs && item.activityTimestamp > endTs) {
return false
}
return true
})
},
filterMovementList(list = [], options = {}) {
const enableChainFilter = options.enableChainFilter !== false
const enableMerchantFilter = options.enableMerchantFilter !== false
const keyword = enableMerchantFilter ? (this.filterForm.merchantKeyword || '').trim().toLowerCase() : ''
const startTs = this.filterForm.startDate ? dayjs(`${this.filterForm.startDate} 00:00:00`).valueOf() : null
const endTs = this.filterForm.endDate ? dayjs(`${this.filterForm.endDate} 23:59:59`).valueOf() : null
return list.filter(item => {
if (enableChainFilter && this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
return false
}
if (keyword) {
const merchantText = String(item.merchantName || '').toLowerCase()
if (!merchantText.includes(keyword)) return false
}
if ((startTs || endTs) && !Number.isFinite(item.submitTimestamp)) {
return false
}
if (startTs && item.submitTimestamp < startTs) {
return false
}
if (endTs && item.submitTimestamp > endTs) {
return false
}
return true
})
},
handleQuery() {
if (!this.validateDateRange()) return
this.fetchList()
},
async handleReset() {
this.filterForm = {
chainId: '',
userGroup: '',
merchantKeyword: '',
startDate: '',
endDate: ''
}
await this.fetchList()
},
openChainPicker() {
this.showChainPicker = true
},
closeChainPicker() {
this.showChainPicker = false
},
onChainConfirm(e) {
const selected = e.value && e.value.length ? e.value[0] : null
this.filterForm.chainId = selected ? (selected.id ?? '') : ''
this.showChainPicker = false
this.handleQuery()
},
openDatePicker(type) {
if (type === 'start') {
this.showStartPicker = true
return
}
this.showEndPicker = true
},
closeDatePicker(type) {
if (type === 'start') {
this.showStartPicker = false
return
}
this.showEndPicker = false
},
setDateValue(type, raw) {
const value = dayjs(raw).format('YYYY-MM-DD')
if (type === 'start') {
this.filterForm.startDate = value
return
}
this.filterForm.endDate = value
},
onDateChange(type, event) {
const raw = event && event.value !== undefined ? event.value : Date.now()
this.setDateValue(type, raw)
},
onDateConfirm(type, event) {
const raw = event && event.value !== undefined ? event.value : Date.now()
const prevStartDate = this.filterForm.startDate
const prevEndDate = this.filterForm.endDate
this.setDateValue(type, raw)
if (!this.validateDateRange()) {
this.filterForm.startDate = prevStartDate
this.filterForm.endDate = prevEndDate
return
}
this.closeDatePicker(type)
this.fetchList()
},
validateDateRange() {
const {startDate, endDate} = this.filterForm
if (!startDate || !endDate) return true
const startTs = dayjs(`${startDate} 00:00:00`).valueOf()
const endTs = dayjs(`${endDate} 23:59:59`).valueOf()
if (startTs <= endTs) return true
uni.showToast({
title: '开始日期不能晚于结束日期',
icon: 'none'
})
return false
},
changeType(value) {
if (value === 'storeVisit') {
this.filterForm.chainId = ''
this.filterForm.merchantKeyword = ''
}
if (value === 'selfReport' && this.activeStatus === 3) {
this.activeStatus = 0
}
this.activeType = value
this.fetchList()
},
changeStatus(value) {
this.activeStatus = value
this.fetchList()
},
async reloadFromFirstPage() {
this.resetPagination()
await this.fetchList()
},
async fetchList(options = {}) {
const append = !!options.append
const requestPage = append ? (this.pageNo + 1) : 1
const pageSize = this.getPageSize()
if (append) {
if (this.loading || this.loadingMore || !this.hasMore) return
this.loadingMore = true
} else {
this.resetPagination()
this.setTaskListByType(this.activeType, [])
this.loading = true
}
try {
const isCopyQuery = this.activeStatus === 3
if (this.activeType === 'movement') {
const params = {
status: isCopyQuery ? 3 : (this.activeStatus + 1),
auditLite: 1
}
if (isCopyQuery) params.isCopy = 1
params.cateIdList = '1,3'
Object.assign(params, this.buildUserTaskFilterParams({
page: requestPage
}))
const {data} = await auditUserTaskPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeTask(item, '药动销'))
const nextList = this.filterMovementList(normalized)
const merged = append ? this.getTaskListByType('movement').concat(nextList) : nextList
this.setTaskListByType('movement', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
} else if (this.activeType === 'storeVisit') {
const params = {
status: isCopyQuery ? 3 : (this.activeStatus + 1),
cateId: 4,
auditLite: 1
}
if (isCopyQuery) params.isCopy = 1
Object.assign(params, this.buildUserTaskFilterParams({
page: requestPage,
enableChainFilter: false,
enableMerchantFilter: false
}))
const {data} = await auditUserTaskPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeTask(item, '跑店/巡店'))
const nextList = this.filterMovementList(normalized, {
enableChainFilter: false,
enableMerchantFilter: false
})
const merged = append ? this.getTaskListByType('storeVisit').concat(nextList) : nextList
this.setTaskListByType('storeVisit', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
} else if (this.activeType === 'selfReport') {
if (isCopyQuery) {
this.setTaskListByType('selfReport', [])
this.hasMore = false
} else {
const params = {
status: this.activeStatus,
page: requestPage,
limit: pageSize,
auditLite: 1
}
Object.assign(params, this.buildSelfReportFilterParams())
const {data} = await selfReportAuditPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeSelfReport(item))
const nextList = this.filterMovementList(normalized)
const merged = append ? this.getTaskListByType('selfReport').concat(nextList) : nextList
this.setTaskListByType('selfReport', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
}
} else {
const params = {
status: isCopyQuery ? 2 : this.activeStatus,
page: requestPage,
limit: pageSize,
auditLite: 1
}
if (isCopyQuery) params.isCopy = 1
Object.assign(params, this.buildShareFilterParams())
const {data} = await shareAuditPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeOrder(item, '药兑换'))
const nextList = this.filterExchangeList(normalized)
const merged = append ? this.getTaskListByType('exchange').concat(nextList) : nextList
this.setTaskListByType('exchange', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.log(err)
} finally {
if (append) {
this.loadingMore = false
} else {
this.loading = false
}
}
},
normalizeTask(item, typeLabel) {
const imageList = Array.isArray(item.imageList) ? item.imageList : this.parseJson(item.image, [])
const chainInfo = this.resolveMovementChain(item)
const task = item && item.task ? item.task : {}
const merchant = item && item.task && item.task.merchant ? item.task.merchant : {}
const user = item && item.user ? item.user : {}
const merchantName = merchant.name || merchant.merchantName || item.merchantName || item.merchant_name || ''
const merchantId = merchant.id ?? merchant.merchantId ?? merchant.merchant_id ?? item.merchantId ?? item.merchant_id ?? ''
const submitRaw = item.submitTime || item.date || item.createdAt || item.createTime || item.create_time || ''
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id,
taskId: item.taskId,
userId: item.userId,
name: item.taskTitle || task.title || '任务',
typeLabel,
taskTitle: item.taskTitle || task.title || '',
taskReward: item.taskReward !== undefined ? item.taskReward : task.reward,
taskTarget: (task.cate && task.cate.title) || task.target || '',
owner,
date: item.date || dayjs(item.date).format('YYYY-MM-DD HH:mm:ss') || item.createdAt || '-',
status: item.status,
imageList: Array.isArray(imageList) ? imageList : [],
merchantName: item.merchantName || merchantName,
merchantId,
chainName: item.chainName || chainInfo.chainName,
chainIdNormalized: chainInfo.chainId,
submitTimestamp: this.parseDateToTimestamp(submitRaw),
submitTime: this.formatDateTime(submitRaw)
}
},
normalizeOrder(item, typeLabel) {
const orderNo = item.orderNo || item.order_no || item.no || item.id
const detailListRaw = item.detailList || item.list || item.items || item.orderItems || []
const detailList = this.parseJson(detailListRaw, [])
const receiptRaw = item.image || item.imageList || item.receiptPhoto || item.receiptPhotos || []
const chainInfo = this.resolveShareChain(item)
const activityRaw = item.date || item.shareDate || item.createTime || item.createdAt || item.create_time || ''
const user = item && item.user ? item.user : {}
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id || item.orderId || item.order_id,
name: orderNo ? `订单:${orderNo}` : '订单',
typeLabel,
owner,
date: this.formatDateTime(activityRaw, '-'),
detailList: Array.isArray(detailList) ? detailList : [],
status: item.status,
amount: item.amount,
income: item.income,
image: item.image,
extraIncome: item.extraIncome,
merchantName: item.merchantName || '',
merchantId: item.merchantId || item.merchant_id || '',
chainName: item.chainName || chainInfo.chainName,
chainIdNormalized: chainInfo.chainId,
activityTimestamp: this.parseDateToTimestamp(activityRaw),
orderNo,
remark: item.remark || '',
userId: item.userId,
}
},
normalizeSelfReport(item) {
const clockPhotos = this.parseJson(item.clockPhotos, [])
const boothPhotos = this.parseJson(item.boothPhotos, [])
const salesGroups = this.parseJson(item.salesGroups, [])
const receiptPhoto = this.parseJson(item.receiptPhoto, [])
const summaryRaw = this.parseJson(item.summary, '')
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw)
const activityDate = item.date || ''
const user = item && item.user ? item.user : {}
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id,
userId: item.userId,
merchantId: item.merchantId,
merchantName: item.merchantName || '',
chainName: item.chainName || '-',
chainIdNormalized: item.chainId || '',
owner,
date: activityDate || '-',
status: item.status,
reward: item.reward,
rating: item.rating,
location: item.location || '',
summary,
rejectReason: item.rejectReason || '',
clockPhotos: Array.isArray(clockPhotos) ? clockPhotos : [],
boothPhotos: this.normalizeReceiptPhotos(boothPhotos),
receiptPhotos: this.normalizeReceiptPhotos(receiptPhoto),
salesGroups: Array.isArray(salesGroups) ? salesGroups : [],
submitTimestamp: this.parseDateToTimestamp(activityDate)
}
},
formatAmount(value) {
const num = Number(value || 0)
return num.toFixed(2)
},
async handleAction(status, item) {
if (Number(status) === 3) {
this.openRejectPopup(item, 'audit')
return
}
await this.submitAuditAction(status, item)
},
async submitAuditAction(status, item, auditReason) {
const data = {
id: item.id,
taskId: item.taskId,
userId: item.userId,
status
}
if (Number(status) === 3) {
data.auditReason = auditReason
}
await changeUserTaskReq(data)
uni.showToast({
title: '操作成功',
})
await this.fetchList()
},
buildSharePayload(item, overrides = {}) {
const base = {...item}
const payload = {
...base,
...overrides
}
delete payload.owner
delete payload.name
delete payload.typeLabel
delete payload.receiptPhotos
delete payload.chainIdNormalized
delete payload.activityTimestamp
if (!Object.prototype.hasOwnProperty.call(overrides, 'detailList')) {
delete payload.detailList
}
return payload
},
sanitizeDetailList(list) {
const items = Array.isArray(list) ? list : []
return items.map(detail => ({
goodsCode: detail.goodsCode || detail.code || '',
goodsName: detail.goodsName || detail.name || detail.title || '',
num: detail.num || detail.qty || '',
spec: detail.spec || detail.specification || '',
amount: detail.amount || '',
sourceCode: detail.sourceCode || detail.source_code || ''
}))
},
async handleActionShare(status, item) {
const data = this.buildSharePayload(item, {status})
await shareChangeReq(data)
uni.showToast({
title: '操作成功',
})
await this.fetchList()
},
async handleShareApprove({item, income}) {
if (!income || Number(income) <= 0) {
uni.showToast({
title: '请确认后再通过',
icon: 'none'
})
return
}
await shareChangeReq({
id: item.id,
status: 1,
})
uni.showToast({
title: '操作成功',
})
await this.fetchList()
},
async handleSelfReportApprove(item) {
await selfReportChangeReq({
id: item.id,
status: 1,
reward: item.reward
})
uni.showToast({
title: '操作成功'
})
await this.reloadFromFirstPage()
},
async handleSelfReportReject(item) {
this.openRejectPopup(item, 'selfReport')
},
async handleSelfReportUpdated() {
await this.reloadFromFirstPage()
},
async handleShareUpdate({item, payload, localDetailList}) {
const data = this.buildSharePayload(item, payload)
await shareChangeReq(data)
uni.showToast({
title: '修改成功',
})
// this.exchangeTasks = this.exchangeTasks.map(task => {
// if (task.id !== item.id) return task
// const orderNo = payload.orderNo || task.orderNo || ''
// return {
// ...task,
// ...payload,
// orderNo,
// name: orderNo ? `订单:${orderNo}` : task.name,
// detailList: Array.isArray(localDetailList) ? localDetailList : task.detailList
// }
// })
await this.fetchList()
},
openRejectPopup(item, type = 'audit') {
this.pendingReject = item
this.pendingRejectType = type
this.rejectReason = ''
this.showRejectPopup = true
},
closeRejectPopup() {
this.showRejectPopup = false
this.rejectReason = ''
this.pendingReject = null
this.pendingRejectType = 'audit'
},
async submitRejectReason() {
const reason = (this.rejectReason || '').trim()
if (this.pendingRejectType !== 'selfReport' && !reason) {
uni.showToast({
title: '请输入拒绝原因',
icon: 'none'
})
return
}
if (!this.pendingReject) {
this.closeRejectPopup()
return
}
if (this.pendingRejectType === 'selfReport') {
await selfReportChangeReq({
id: this.pendingReject.id,
status: 2,
rejectReason: reason
})
uni.showToast({
title: '操作成功'
})
await this.reloadFromFirstPage()
this.closeRejectPopup()
return
}
await this.submitAuditAction(3, this.pendingReject, reason)
this.closeRejectPopup()
},
preview(url, list) {
uni.previewImage({
current: url,
urls: list
})
},
parseJson(value, fallback) {
if (value === null || value === undefined || value === '') return fallback
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch (err) {
return value
}
}
return value
},
parseDateToTimestamp(value) {
if (value === null || value === undefined || value === '') return NaN
const raw = String(value).trim()
if (!raw) return NaN
if (/^-?\d+(\.\d+)?$/.test(raw)) {
const num = Number(raw)
if (!Number.isFinite(num)) return NaN
return Math.abs(num) < 1e12 ? num * 1000 : num
}
const parsed = dayjs(raw)
return parsed.isValid() ? parsed.valueOf() : NaN
},
formatDateTime(value, fallback = '') {
if (value === null || value === undefined || value === '') return fallback
const raw = String(value).trim()
if (!raw) return fallback
if (/^-?\d+(\.\d+)?$/.test(raw)) {
const num = Number(raw)
if (!Number.isFinite(num)) return fallback
const parsedNumber = dayjs(Math.abs(num) < 1e12 ? num * 1000 : num)
return parsedNumber.isValid() ? parsedNumber.format('YYYY-MM-DD HH:mm:ss') : fallback
}
const parsed = dayjs(raw)
if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss')
return raw.replace('T', ' ')
},
resolveShareChain(item) {
const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? ''
const merchantId = item.merchantId ?? item.merchant_id ?? ''
const directName = item.chainName || item.chain_name || item.chain || item.chainAbb || ''
let chainId = rawChainId
let chainName = directName
const merchantChain = merchantId !== '' && merchantId !== null && merchantId !== undefined
? this.chainMerchantMap[String(merchantId)]
: null
if (!chainId && merchantChain) {
chainId = merchantChain.chainId
}
if (!chainName && merchantChain) {
chainName = merchantChain.chainName
}
if (!chainName && chainId !== '' && chainId !== null && chainId !== undefined) {
const selected = this.findChainOption(chainId, chainName)
chainName = selected ? selected.text : ''
}
return {
chainId: chainId ?? '',
chainName: chainName || '-'
}
},
resolveMovementChain(item) {
const task = item && item.task ? item.task : {}
const merchant = task && task.merchant ? task.merchant : {}
const rawChainId = item.chainId
?? item.chain_id
?? task.chainId
?? task.chain_id
?? merchant.chainId
?? merchant.chain_id
?? merchant.chainCode
?? merchant.chain_code
?? ''
const merchantId = item.merchantId
?? item.merchant_id
?? task.merchantId
?? task.merchant_id
?? merchant.id
?? merchant.merchantId
?? merchant.merchant_id
?? ''
const directName = item.chainName
|| item.chain_name
|| task.chainName
|| task.chain_name
|| merchant.chainName
|| merchant.chain_name
|| merchant.chain
|| merchant.chainAbb
|| ''
let chainId = rawChainId
let chainName = directName
const merchantChain = merchantId !== '' && merchantId !== null && merchantId !== undefined
? this.chainMerchantMap[String(merchantId)]
: null
if (!chainId && merchantChain) {
chainId = merchantChain.chainId
}
if (!chainName && merchantChain) {
chainName = merchantChain.chainName
}
if (!chainName && chainId !== '' && chainId !== null && chainId !== undefined) {
const selected = this.findChainOption(chainId, chainName)
chainName = selected ? selected.text : ''
}
return {
chainId: chainId ?? '',
chainName: chainName || '-'
}
},
normalizeChainDictOption(item, index) {
const text = item.dictDataName || item.label || item.name || item.title || item.text || `连锁${index + 1}`
const code = item.dictDataCode ?? item.code ?? item.value ?? ''
const value = item.dictDataValue ?? item.value ?? item.code ?? ''
const rawId = item.id ?? code ?? value ?? `chain_${index}`
return {
id: rawId,
text,
code,
value
}
},
findChainOption(chainId, chainName = '') {
const chainIdText = String(chainId ?? '').trim()
const chainNameText = String(chainName || '').trim()
return this.chainOptions.find(option => {
const candidates = [
option.id,
option.code,
option.value,
option.text
]
.map(value => String(value ?? '').trim())
.filter(Boolean)
if (chainIdText && candidates.includes(chainIdText)) return true
return !!chainNameText && candidates.includes(chainNameText)
}) || null
},
matchesSelectedChain(chainId, chainName, selectedId) {
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
if (!option) {
return String(chainId ?? '').trim() === String(selectedId).trim()
}
const selectedValues = [
option.id,
option.code,
option.value,
option.text
]
.map(value => String(value ?? '').trim())
.filter(Boolean)
const itemValues = [
chainId,
chainName
]
.map(value => String(value ?? '').trim())
.filter(Boolean)
return itemValues.some(value => selectedValues.includes(value))
},
getSelectedChainApiValue(selectedId) {
if (selectedId === '' || selectedId === null || selectedId === undefined) return ''
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
if (!option) return selectedId
return option.code || option.value || option.id
},
normalizeMerchantChain(item, index) {
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? ''
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++) {
const list = item[childCandidates[i]]
if (Array.isArray(list) && list.length) {
rawChildren = list
break
}
}
const merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
return {
id: chainId,
text: chainName,
merchants
}
},
normalizeMerchant(item) {
const id = item.id ?? item.merchantId ?? item.merchant_id
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
return {
id,
text
}
},
pickFirstText(source, keys = []) {
if (!source || typeof source !== 'object') return ''
for (let i = 0; i < keys.length; i++) {
const value = source[keys[i]]
if (value !== undefined && value !== null && String(value).trim()) {
return String(value).trim()
}
}
return ''
},
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++) {
const value = data[keys[i]]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
const nested = this.unwrapList(value)
if (nested.length) return nested
}
}
return [data]
},
resolveMediaUrl(media) {
if (!media) return ''
if (typeof media === 'string') return media
return media.url || media.path || media.image || ''
},
normalizeReceiptPhotos(value) {
if (!value) return []
const list = Array.isArray(value) ? value : [value]
return list.map(item => this.resolveMediaUrl(item)).filter(Boolean)
}
}
}
</script>
<style lang="scss" scoped>
.audit-page {
min-height: 100vh;
//padding: 0 20rpx 40rpx;
background: #f5f5f5;
}
.page-hero {
//padding: 30rpx 6rpx 18rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.subsection {
margin: 18rpx 20rpx 0;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.type-tabs,
.status-tabs {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
}
.tab {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: #f5f8ff;
color: var(--text-sub);
font-size: 24rpx;
border: 1rpx solid #e1e8f5;
}
.tab.active {
color: #ffffff;
background: var(--brand-primary);
border-color: var(--brand-primary);
}
.block {
margin: 18rpx 20rpx 0;
}
.filter-row {
margin-top: 14rpx;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.filter-chip {
padding: 8rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
color: #2fc6a7;
font-size: 22rpx;
background: #f3fffb;
}
.group-input {
flex: 1;
min-width: 180rpx;
height: 52rpx;
padding: 0 18rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
background: #f3fffb;
color: #374151;
font-size: 22rpx;
}
.keyword-row {
margin-top: 14rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.keyword-input {
flex: 1;
height: 64rpx;
border-radius: 14rpx;
border: 1rpx solid #d1d5db;
background: #ffffff;
padding: 0 16rpx;
font-size: 24rpx;
color: #374151;
}
.keyword-placeholder {
color: #9ca3af;
}
.filter-btn {
min-width: 96rpx;
height: 64rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid #d1d5db;
color: #6b7280;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
.filter-btn.primary {
border-color: #2fc6a7;
background: #2fc6a7;
color: #ffffff;
}
.action-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14rpx;
}
.action-btn {
font-size: 30rpx;
}
.danger-btn {
background: linear-gradient(135deg, #f5576c, #f093fb);
color: #ffffff;
border-radius: 999rpx;
padding: 18rpx 32rpx;
text-align: center;
}
.tip {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 12rpx;
}
.empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
.load-more-tip {
color: #9ca3af;
text-align: center;
font-size: 22rpx;
padding: 24rpx 0 32rpx;
}
</style>