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

1414 lines
47 KiB
Vue
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.
<template>
<view>
<view class="record">
<view>
<view class="record-meta" v-if="currentRecord.chainName && currentRecord.chainName !== '-'">连锁:{{ currentRecord.chainName }}</view>
<view class="record-meta-row">
<view class="record-meta">门店:{{ currentRecord.merchantName || '-' }}</view>
<view v-if="item.status === 0" class="record-meta-action" @tap="openMerchantEditPicker">修改</view>
</view>
<view class="record-meta-row">
<view class="record-meta">活动日期:{{ currentRecord.date || item.date || '-' }}</view>
<view v-if="item.status === 0" class="record-meta-action" @tap="openDateEditPicker">修改</view>
</view>
<!-- <view class="record-meta" style="color: red">奖金:¥{{ formatPrice(currentRecord.reward || 0) }}</view>-->
<view class="record-meta">评分:{{ currentRecord.rating || '-' }} 星</view>
<view class="record-meta">提交人:{{ currentRecord.owner || item.owner }}</view>
<view class="record-meta">小结:{{ currentRecord.summary || '-' }}</view>
<view class="record-meta text-red" v-if="currentRecord.rejectReason">驳回原因:{{ currentRecord.rejectReason }}</view>
<view class="record-toggle" @tap="toggleDetailExpanded">
<text>{{ detailExpanded ? '收起详情' : '展开详情' }}</text>
<u-icon :name="detailExpanded ? 'arrow-up' : 'arrow-down'" size="20" color="#2fc6a7"/>
</view>
<view v-if="detailExpanded" class="record-detail">
<view class="record-section">
<view class="record-section-title">上下班打卡</view>
<view v-if="clockImageList.length" class="media-images">
<image
v-for="(img, idx) in clockImageList"
:key="`clock-${idx}`"
class="media-image"
:src="img"
mode="aspectFill"
@click="emitPreview(img, clockImageList)"
/>
</view>
<view v-else class="detail-empty">暂无打卡图片</view>
</view>
<view class="record-section">
<view class="record-section-title">门头摆台照</view>
<view v-if="boothImageList.length" class="media-images">
<image
v-for="(img, idx) in boothImageList"
:key="`booth-${idx}`"
class="media-image"
:src="img"
mode="aspectFill"
@click="emitPreview(img, boothImageList)"
/>
</view>
<view v-else class="detail-empty">暂无门头摆台照</view>
</view>
<view class="record-section">
<view class="record-section-title">小票上传</view>
<view v-if="receiptImageList.length" class="media-images">
<image
v-for="(img, idx) in receiptImageList"
:key="`receipt-${idx}`"
class="media-image"
:src="img"
mode="aspectFill"
@click="emitPreview(img, receiptImageList)"
/>
</view>
<view v-else class="detail-empty">暂无小票</view>
</view>
<view class="record-section">
<view class="record-section-title-row">
<view class="record-section-title">销量填写</view>
<view v-if="item.status === 0" class="section-action" @tap="openEditPopup">修改销量</view>
</view>
<view v-if="salesRows.length" class="detail-list">
<view class="detail-item" v-for="(detail, idx) in salesRows" :key="`sales-${idx}`">
<view class="detail-head">
<view class="detail-name">{{ detail.name }}</view>
<view class="detail-value text-red">销量:{{ detail.qty }}{{ detail.unit }}</view>
</view>
<view class="detail-grid">
<view class="detail-kv">
<text class="detail-label">分组</text>
<text class="detail-value">{{ detail.groupTitle }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">厂家</text>
<text class="detail-value">{{ detail.makerAbb || '-' }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">规格</text>
<text class="detail-value">{{ detail.spec || '-' }}</text>
</view>
</view>
</view>
</view>
<view v-else class="detail-empty">暂无销量数据</view>
</view>
</view>
</view>
<view class="record-actions" v-if="item.status === 0">
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="handleApprove">通过</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p danger-chip" @tap="$emit('reject', item)">驳回</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="openEditPopup">修改销量</view>
</view>
</view>
</view>
<view v-if="showEditPopup" class="self-report-popup-mask" @tap="closeEditPopup" @touchmove.stop.prevent></view>
<view v-if="showEditPopup" class="self-report-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title">修改销量填写</view>
<view class="close-btn" @tap="closeEditPopup">关闭</view>
</view>
<!-- <view class="edit-summary">-->
<!-- <view class="edit-summary-label">奖金合计</view>-->
<!-- <view class="edit-summary-value">¥{{ editTotalRewardDisplay }}</view>-->
<!-- </view>-->
<view class="detail-actions">
<view class="ghost-btn" @tap="openProductPicker(-1)">新增商品</view>
</view>
<view v-if="editSalesItems.length === 0" class="detail-empty">暂无销量商品</view>
<view v-else class="edit-sales-list">
<view class="edit-sales-item" v-for="(detail, idx) in editSalesItems" :key="detail._key || idx">
<view class="edit-sales-head">
<view>
<view class="edit-sales-name">{{ detail.name || '商品' }}</view>
<view class="edit-sales-group">{{ detail.groupTitle || '自定义商品' }}</view>
</view>
<view class="edit-sales-remove" @tap="removeEditSalesItem(idx)">删除</view>
</view>
<view class="form-row">
<view class="label">商品</view>
<view class="picker-input" @tap="openProductPicker(idx)">
<view class="picker-input-inner">
<text :class="detail.name ? 'picker-text' : 'picker-placeholder'">{{ detail.name || '点击选择商品' }}</text>
<u-icon name="arrow-right" size="20" color="#9ca3af"/>
</view>
</view>
</view>
<view class="detail-grid edit-grid">
<view class="detail-kv">
<text class="detail-label">厂家</text>
<text class="detail-value">{{ detail.makerAbb || '-' }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">编码</text>
<text class="detail-value">{{ detail.goodsCode || '-' }}</text>
</view>
</view>
<view class="form-row" v-if="detail.specOptions && detail.specOptions.length">
<view class="label">规格</view>
<picker class="picker-input" mode="selector" :range="detail.specOptions" @change="onSpecChange($event, detail)">
<view class="picker-input-inner">
<text class="picker-text">{{ detail.spec || detail.specOptions[0] || '规格' }}</text>
<u-icon name="arrow-right" size="20" color="#9ca3af"/>
</view>
</picker>
</view>
<view class="form-row" v-else>
<view class="label">规格</view>
<input v-model="detail.spec" class="input" placeholder="规格"/>
</view>
<!-- <view class="form-row">-->
<!-- <view class="label">单品奖金</view>-->
<!-- <view class="form-value bonus-text">¥{{ formatMoney(getTaskIncomeNumber(detail)) }}</view>-->
<!-- </view>-->
<view class="form-row">
<view class="label">数量</view>
<view class="qty-input-wrap">
<input :value="String(detail.qty === undefined || detail.qty === null ? '' : detail.qty)" class="input qty-input" type="digit" placeholder="0" @input="onEditQtyInput($event, idx)"/>
<text class="qty-unit">{{ detail.unit || '盒' }}</text>
</view>
</view>
</view>
</view>
<view class="popup-actions">
<view class="primary-btn submit" @tap="saveEdit">{{ editSaving ? '保存中...' : '保存' }}</view>
</view>
</view>
<u-popup :show="showMerchantEditPicker" mode="bottom" round="24rpx" @close="closeMerchantEditPicker">
<view class="merchant-edit-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeMerchantEditPicker">取消</view>
<text class="picker-title">修改门店</text>
<view class="picker-action primary" @tap="saveMerchantEdit">{{ merchantEditSaving ? '保存中...' : '保存' }}</view>
</view>
<view v-if="merchantEditLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<view v-else>
<view class="merchant-picker-search">
<input v-model="merchantEditKeyword" class="merchant-search-input" placeholder="搜索门店" placeholder-class="merchant-search-placeholder" @input="onMerchantEditSearchInput"/>
</view>
<picker-view class="merchant-picker-view" :value="merchantEditPickerValue" @change="onMerchantEditPickerChange">
<picker-view-column>
<view v-for="(chain, index) in merchantEditChains" :key="`edit-chain-${index}`" class="merchant-picker-item">
{{ chain.text }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="(merchant, index) in merchantEditStores" :key="`edit-merchant-${index}`" class="merchant-picker-item">
{{ merchant.text }}
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</u-popup>
<u-datetime-picker
v-if="showDateEditPicker"
:key="dateEditPickerKey"
:show="showDateEditPicker"
:value="dateEditPickerValue"
mode="date"
@confirm="saveDateEdit"
@cancel="closeDateEditPicker"
@close="closeDateEditPicker"
/>
<u-popup :show="showProductPicker" mode="bottom" round="24rpx" @close="closeProductPicker">
<view class="product-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeProductPicker">取消</view>
<text class="picker-title">选择商品</text>
<view class="picker-action primary" @tap="closeProductPicker">完成</view>
</view>
<view class="merchant-picker-search">
<input
v-model="productKeyword"
class="merchant-search-input"
placeholder="搜索商品名称/编码/厂家/规格"
placeholder-class="merchant-search-placeholder"
/>
</view>
<view v-if="productLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<view v-else class="product-panel">
<scroll-view class="category-sidebar" scroll-y>
<view
v-for="group in filteredProductGroups"
:key="group.key"
class="category-item"
:class="currentProductGroupKey === group.key ? 'category-item-active' : ''"
@tap="selectProductGroup(group.key)"
>
{{ group.title }}
</view>
</scroll-view>
<scroll-view class="product-panel-main" scroll-y>
<view v-if="filteredProductGroups.length === 0" class="detail-empty">暂无可选商品</view>
<view v-else-if="displayedProductOptions.length === 0" class="detail-empty">该分类暂无商品</view>
<view v-else class="product-picker-list">
<view
v-for="product in displayedProductOptions"
:key="product._key"
class="product-picker-item"
@tap="selectProduct(product)"
>
<view class="product-picker-main">
<view class="product-picker-name">{{ product.name || '-' }}</view>
<view class="product-picker-sub">{{ product.makerAbb || '-' }} / {{ product.spec || '规格' }}</view>
<view class="product-picker-sub">编码:{{ product.goodsCode || '-' }}</view>
<!-- <view class="product-picker-income">奖金金额:¥{{ formatMoney(getTaskIncomeNumber(product)) }}</view>-->
</view>
<view class="product-picker-tag">{{ isProductSelected(product) ? '已添加' : '添加' }}</view>
</view>
</view>
</scroll-view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import dayjs from 'dayjs'
import {productGroupByCateReq} from "@/api/product";
import {merchantGroupByChainReq} from "@/api/shop";
import {selfReportAuditDetailReq, selfReportChangeReq} from "@/api/self-report";
export default {
name: 'AuditSelfReportItem',
props: {
item: {
type: Object,
required: true
}
},
data() {
return {
detailExpanded: false,
detailLoading: false,
detailLoaded: false,
detailData: null,
showEditPopup: false,
editSaving: false,
showProductPicker: false,
productLoading: false,
productGroups: [],
productLoadedChainName: '',
productKeyword: '',
activeProductGroupKey: '',
editSalesItems: [],
activeEditSalesIndex: -1,
showMerchantEditPicker: false,
merchantEditLoading: false,
merchantEditSaving: false,
merchantEditChains: [],
merchantEditPickerValue: [0, 0],
merchantEditKeyword: '',
showDateEditPicker: false,
dateEditPickerValue: Date.now(),
dateEditPickerKey: 0,
dateEditSaving: false
}
},
computed: {
currentRecord() {
return this.detailData || this.item
},
clockImageList() {
const source = this.currentRecord
return this.parseClockImageList(source.clockPhotos)
},
boothImageList() {
const source = this.currentRecord
return this.parseImageList(source.boothPhotos)
},
receiptImageList() {
const source = this.currentRecord
return this.parseImageList(source.receiptPhotos)
},
salesRows() {
const source = this.currentRecord
const groups = Array.isArray(source.salesGroups) ? source.salesGroups : []
const rows = []
groups.forEach((group, gIdx) => {
const items = Array.isArray(group && group.items) ? group.items : []
items.forEach((detail, iIdx) => {
rows.push({
key: `${gIdx}-${iIdx}`,
groupTitle: group && group.title ? group.title : `分组${gIdx + 1}`,
name: detail && detail.name ? detail.name : '-',
makerAbb: detail && detail.makerAbb ? detail.makerAbb : '',
spec: detail && detail.spec ? detail.spec : '',
qty: detail && detail.qty !== undefined && detail.qty !== null ? detail.qty : 0,
unit: detail && detail.unit ? detail.unit : ''
})
})
})
return rows
},
filteredProductGroups() {
const keyword = (this.productKeyword || '').trim().toLowerCase()
const groups = Array.isArray(this.productGroups) ? this.productGroups : []
if (!keyword) return groups
return groups
.map(group => {
const items = Array.isArray(group.items) ? group.items : []
const nextItems = items.filter(item => {
const name = String(item.name || '').toLowerCase()
const maker = String(item.makerAbb || '').toLowerCase()
const spec = String(item.spec || '').toLowerCase()
const code = String(item.goodsCode || '').toLowerCase()
return name.includes(keyword) || maker.includes(keyword) || spec.includes(keyword) || code.includes(keyword)
})
return Object.assign({}, group, {items: nextItems})
})
.filter(group => Array.isArray(group.items) && group.items.length)
},
currentProductGroupKey() {
const groups = Array.isArray(this.filteredProductGroups) ? this.filteredProductGroups : []
if (!groups.length) return ''
const exists = groups.some(group => group.key === this.activeProductGroupKey)
return exists ? this.activeProductGroupKey : groups[0].key
},
displayedProductOptions() {
const groups = Array.isArray(this.filteredProductGroups) ? this.filteredProductGroups : []
const current = groups.find(group => group.key === this.currentProductGroupKey)
return current && Array.isArray(current.items) ? current.items : []
},
editTotalRewardAmount() {
return (Array.isArray(this.editSalesItems) ? this.editSalesItems : []).reduce((sum, item) => {
return sum + (this.parseNumber(item && item.qty) * this.getTaskIncomeNumber(item))
}, 0)
},
editTotalRewardDisplay() {
return this.formatMoney(this.editTotalRewardAmount)
},
merchantEditStores() {
const chainIndex = Number(this.merchantEditPickerValue[0] || 0)
const chain = this.merchantEditChains[chainIndex] || this.merchantEditChains[0] || {}
const stores = Array.isArray(chain.merchants) ? chain.merchants : []
const keyword = this.merchantEditKeyword.trim()
const filteredStores = keyword ? stores.filter(store => String(store.text || '').includes(keyword)) : stores
return filteredStores.length ? filteredStores : [{id: '', text: keyword ? '无匹配门店' : '暂无门店'}]
}
},
methods: {
async toggleDetailExpanded() {
const nextExpanded = !this.detailExpanded
this.detailExpanded = nextExpanded
if (!nextExpanded || this.detailLoaded || this.detailLoading || !this.item.id) return
this.detailLoading = true
try {
const {data} = await selfReportAuditDetailReq(this.item.id)
this.detailData = this.normalizeDetailData(data)
this.detailLoaded = true
} catch (error) {
uni.showToast({
title: '详情加载失败',
icon: 'none'
})
} finally {
this.detailLoading = false
}
},
emitPreview(url, list) {
this.$emit('preview', url, list)
},
handleApprove() {
this.$emit('approve', this.currentRecord)
},
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 ''
},
parseNumber(value) {
const text = String(value === null || value === undefined ? '' : value).trim()
if (!text) return 0
const num = Number(text)
return Number.isFinite(num) ? num : 0
},
formatMoney(value) {
const num = Number(value)
return Number.isFinite(num) ? num.toFixed(2) : '0.00'
},
getTaskIncomeNumber(item) {
if (!item || typeof item !== 'object') return 0
const num = Number(item.taskIncome !== undefined && item.taskIncome !== null ? item.taskIncome : item.income)
return Number.isFinite(num) ? num : 0
},
normalizeSalesGroupItem(detail = {}, groupTitle = '') {
const product = detail.product && typeof detail.product === 'object' ? detail.product : {}
const specOptions = Array.isArray(detail.specOptions) && detail.specOptions.length
? detail.specOptions
: Array.isArray(product.specOptions) && product.specOptions.length
? product.specOptions
: []
const productId = detail.productId || detail.goodsId || product.productId || product.goodsId || product.id || ''
const goodsCode = detail.goodsCode || detail.code || detail.productCode || product.goodsCode || product.code || product.productCode || ''
const name = detail.name || detail.goodsName || detail.title || product.goodsName || product.name || product.title || ''
const makerAbb = detail.makerAbb || detail.manufacturer || product.makerAbb || product.manufacturer || ''
const spec = detail.spec || detail.specification || product.spec || specOptions[0] || ''
const taskIncome = detail.taskIncome !== undefined && detail.taskIncome !== null
? detail.taskIncome
: product.income !== undefined && product.income !== null
? product.income
: detail.income || 0
return {
_key: detail._key || `${Date.now()}_${Math.random().toString(16).slice(2)}`,
groupTitle: groupTitle || detail.groupTitle || '自定义商品',
productId,
goodsCode,
name,
makerAbb,
spec,
specOptions,
unit: detail.unit || product.unit || '盒',
qty: detail.qty !== undefined && detail.qty !== null ? detail.qty : detail.num || 0,
taskIncome
}
},
flattenSalesGroups(groups) {
const source = Array.isArray(groups) ? groups : []
const rows = []
source.forEach((group, gIdx) => {
const items = Array.isArray(group && group.items) ? group.items : []
items.forEach((detail, iIdx) => {
rows.push(this.normalizeSalesGroupItem(Object.assign({}, detail, {
_key: detail && detail._key ? detail._key : `${gIdx}_${iIdx}_${Date.now()}`
}), group && group.title ? group.title : `分组${gIdx + 1}`))
})
})
return rows
},
buildSalesGroupsFromItems(items) {
const groupsMap = {}
;(Array.isArray(items) ? items : []).forEach(item => {
const groupTitle = item.groupTitle || '自定义商品'
if (!groupsMap[groupTitle]) {
groupsMap[groupTitle] = {
title: groupTitle,
items: []
}
}
groupsMap[groupTitle].items.push({
productId: item.productId || '',
goodsCode: item.goodsCode || '',
name: item.name || '',
makerAbb: item.makerAbb || '',
spec: item.spec || '',
taskIncome: this.getTaskIncomeNumber(item),
qty: this.parseNumber(item.qty),
unit: item.unit || '盒',
specOptions: Array.isArray(item.specOptions) ? item.specOptions : []
})
})
return Object.keys(groupsMap).map(key => groupsMap[key])
},
unwrapProductList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'children', 'productGroup', 'productList', 'products']
for (let i = 0; i < keys.length; i += 1) {
if (Array.isArray(data[keys[i]])) return data[keys[i]]
}
return [data]
},
isProductNode(node) {
if (!node || typeof node !== 'object') return false
const product = node.product && typeof node.product === 'object' ? node.product : {}
const hasChildrenOnly = Array.isArray(node.children)
&& !this.pickFirstText(node, ['goodsName', 'goods_name', 'goodsCode', 'code', 'productCode', 'batchCode'])
&& !node.product
if (hasChildrenOnly) return false
return !!(
this.pickFirstText(node, ['goodsName', 'goods_name', 'goodsCode', 'code', 'productCode', 'batchCode']) ||
this.pickFirstText(product, ['goodsName', 'goods_name', 'goodsCode', 'code', 'productCode', 'batchCode'])
)
},
collectProductNodes(node, depth = 0) {
if (!node || depth > 4) return []
if (Array.isArray(node)) {
return node.reduce((acc, item) => acc.concat(this.collectProductNodes(item, depth + 1)), [])
}
if (typeof node !== 'object') return []
if (this.isProductNode(node)) return [node]
const keys = ['productGroup', 'productList', 'products', 'list', 'records', 'rows', 'items', 'children', 'data']
return keys.reduce((acc, key) => {
const value = node[key]
if (Array.isArray(value) || (value && typeof value === 'object')) {
return acc.concat(this.collectProductNodes(value, depth + 1))
}
return acc
}, [])
},
normalizeSpecOptions(item) {
const raw = item.specOptions || item.specList || item.specs || []
if (Array.isArray(raw)) {
return raw.map(value => `${value}`.trim()).filter(Boolean)
}
if (typeof raw === 'string') {
return raw.split(/[、,,]/).map(value => value.trim()).filter(Boolean)
}
const spec = item.spec || item.specification || item.standard || ''
return spec ? [spec] : []
},
normalizeProductNode(item = {}) {
const product = item && item.product ? item.product : {}
const goodsCode = item.goodsCode || item.code || item.productCode || item.batchCode || product.code || product.goodsCode || product.batchCode || ''
const productId = item.productId || item.id || item.goodsId || item.skuId || product.productId || product.id || product.goodsId || product.skuId || ''
const name = item.goodsName || item.goods_name || item.title || item.name || product.title || product.goodsName || product.name || ''
const makerAbb = item.makerAbb || item.maker || item.manufacturer || product.makerAbb || product.maker || product.manufacturer || ''
const spec = product.spec || item.spec || item.specification || ''
return {
productId,
goodsCode,
name,
makerAbb,
spec,
unit: item.unit || product.unit || '盒',
taskIncome: item.taskIncome !== undefined && item.taskIncome !== null ? item.taskIncome : (product.income || item.income || 0),
specOptions: this.normalizeSpecOptions(Object.assign({}, product, item))
}
},
buildProductGroupsFromData(rawData) {
const rootList = this.unwrapProductList(rawData)
const tabs = []
rootList.forEach((categoryNode, categoryIndex) => {
if (!categoryNode || typeof categoryNode !== 'object') return
const label = categoryNode.cate || categoryNode.title || categoryNode.name
const products = Array.isArray(categoryNode.children) ? categoryNode.children : []
if (!label || !products.length) return
tabs.push({
key: `cate_children_${categoryIndex}`,
label,
rawProducts: products
})
})
if (!tabs.length) {
const allProducts = this.collectProductNodes(rootList)
if (allProducts.length) {
tabs.push({
key: 'all',
label: '全部',
rawProducts: allProducts
})
}
}
let rowSeed = 0
return tabs.map((tab, tabIndex) => {
const items = (Array.isArray(tab.rawProducts) ? tab.rawProducts : [])
.map((rawItem, itemIndex) => {
const product = this.normalizeProductNode(rawItem)
if (!product.name) return null
const specOptions = Array.isArray(product.specOptions) && product.specOptions.length
? product.specOptions
: [product.spec || '规格']
return {
...product,
groupTitle: tab.label || `分类${tabIndex + 1}`,
spec: product.spec || specOptions[0] || '规格',
specOptions,
_key: `audit_product_${tabIndex}_${itemIndex}_${rowSeed++}`
}
})
.filter(Boolean)
return {
key: tab.key || `group_${tabIndex}`,
title: tab.label || `分类${tabIndex + 1}`,
items
}
}).filter(group => group.items.length)
},
async loadProductGroups() {
const chainName = String(this.currentRecord.chainName || '').trim()
if (!chainName || chainName === '-') {
this.productGroups = []
this.productLoadedChainName = ''
this.activeProductGroupKey = ''
return
}
if (this.productLoadedChainName === chainName && this.productGroups.length) return
this.productLoading = true
try {
const {data} = await productGroupByCateReq({chain: chainName, type: 0})
this.productGroups = this.buildProductGroupsFromData(data)
this.productLoadedChainName = chainName
this.activeProductGroupKey = this.productGroups[0] ? this.productGroups[0].key : ''
} catch (error) {
this.productGroups = []
this.productLoadedChainName = ''
this.activeProductGroupKey = ''
uni.showToast({
title: '商品数据获取失败',
icon: 'none'
})
} finally {
this.productLoading = false
}
},
async ensureDetailLoaded() {
if (this.detailLoaded || this.detailLoading || !this.item.id) return
this.detailLoading = true
try {
const {data} = await selfReportAuditDetailReq(this.item.id)
this.detailData = this.normalizeDetailData(data)
this.detailLoaded = true
} catch (error) {
uni.showToast({
title: '详情加载失败',
icon: 'none'
})
} finally {
this.detailLoading = false
}
},
unwrapMerchantList(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 = this.unwrapMerchantList(value)
if (nested.length) return nested
}
}
return [data]
},
normalizeMerchant(item = {}) {
return {
id: item.id ?? item.merchantId ?? item.merchant_id ?? '',
text: item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
}
},
normalizeMerchantChain(item = {}, index) {
const text = item.chainName || item.chain || item.chainAbb || item.name || item.title || `连锁${index + 1}`
const id = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? `chain_${index}`
const childKeys = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
let children = []
for (let i = 0; i < childKeys.length; i += 1) {
if (Array.isArray(item[childKeys[i]]) && item[childKeys[i]].length) {
children = item[childKeys[i]]
break
}
}
let merchants = children
.map(child => this.normalizeMerchant(child))
.filter(merchant => merchant.id !== '' && merchant.id !== null && merchant.id !== undefined)
if (!merchants.length) {
const merchant = this.normalizeMerchant(item)
if (merchant.id !== '' && merchant.id !== null && merchant.id !== undefined) {
merchants = [merchant]
}
}
return {id, text, merchants}
},
async loadMerchantEditChains() {
if (this.merchantEditChains.length) return
try {
const {data} = await merchantGroupByChainReq({})
this.merchantEditChains = this.unwrapMerchantList(data)
.map((item, index) => this.normalizeMerchantChain(item, index))
.filter(chain => chain.merchants.length)
} catch (error) {
this.merchantEditChains = []
uni.showToast({title: '门店列表获取失败', icon: 'none'})
}
},
async openMerchantEditPicker() {
if (this.item.status !== 0) return
this.showMerchantEditPicker = true
this.merchantEditKeyword = ''
this.merchantEditLoading = true
try {
await this.ensureDetailLoaded()
await this.loadMerchantEditChains()
if (!this.merchantEditChains.length) {
this.showMerchantEditPicker = false
return
}
const record = this.currentRecord
const chainIndex = this.merchantEditChains.findIndex(chain => String(chain.id) === String(record.chainId))
const safeChainIndex = chainIndex > -1 ? chainIndex : 0
const stores = this.merchantEditChains[safeChainIndex].merchants
const merchantIndex = stores.findIndex(merchant => String(merchant.id) === String(record.merchantId))
this.merchantEditPickerValue = [safeChainIndex, merchantIndex > -1 ? merchantIndex : 0]
} finally {
this.merchantEditLoading = false
}
},
closeMerchantEditPicker() {
if (!this.merchantEditSaving) {
this.showMerchantEditPicker = false
this.merchantEditKeyword = ''
}
},
onMerchantEditPickerChange(event) {
const value = Array.isArray(event.detail.value) ? event.detail.value : [0, 0]
const chainIndex = Number(value[0] || 0)
const previousChainIndex = Number(this.merchantEditPickerValue[0] || 0)
this.merchantEditPickerValue = [chainIndex, chainIndex === previousChainIndex ? Number(value[1] || 0) : 0]
},
onMerchantEditSearchInput() {
this.merchantEditPickerValue = [Number(this.merchantEditPickerValue[0] || 0), 0]
},
async saveMerchantEdit() {
if (this.merchantEditSaving) return
const chainIndex = Number(this.merchantEditPickerValue[0] || 0)
const merchantIndex = Number(this.merchantEditPickerValue[1] || 0)
const chain = this.merchantEditChains[chainIndex]
const merchant = this.merchantEditStores[merchantIndex]
if (!chain || !merchant || !merchant.id) {
uni.showToast({title: '请选择门店', icon: 'none'})
return
}
this.merchantEditSaving = true
try {
await selfReportChangeReq({
id: this.currentRecord.id,
chainId: chain.id,
chainName: chain.text,
merchantId: merchant.id,
merchantName: merchant.text
})
this.detailData = Object.assign({}, this.currentRecord, {
chainId: chain.id,
chainName: chain.text,
merchantId: merchant.id,
merchantName: merchant.text
})
this.detailLoaded = true
this.showMerchantEditPicker = false
this.$emit('updated', this.detailData)
uni.showToast({title: '保存成功', icon: 'success'})
} catch (error) {
uni.showToast({title: error && error.message ? error.message : '保存失败', icon: 'none'})
} finally {
this.merchantEditSaving = false
}
},
async openDateEditPicker() {
if (this.item.status !== 0) return
await this.ensureDetailLoaded()
const value = dayjs(this.currentRecord.date || this.item.date)
this.dateEditPickerValue = value.isValid() ? value.valueOf() : Date.now()
this.dateEditPickerKey += 1
await this.$nextTick()
this.showDateEditPicker = true
},
closeDateEditPicker() {
if (!this.dateEditSaving) this.showDateEditPicker = false
},
async saveDateEdit(event) {
if (this.dateEditSaving) return
const date = dayjs(event.value).format('YYYY-MM-DD')
this.dateEditSaving = true
try {
await selfReportChangeReq({id: this.currentRecord.id, date})
this.detailData = Object.assign({}, this.currentRecord, {date})
this.detailLoaded = true
this.showDateEditPicker = false
this.$emit('updated', this.detailData)
uni.showToast({title: '保存成功', icon: 'success'})
} catch (error) {
uni.showToast({title: error && error.message ? error.message : '保存失败', icon: 'none'})
} finally {
this.dateEditSaving = false
}
},
async openEditPopup() {
if (this.item.status !== 0) return
await this.ensureDetailLoaded()
const salesGroups = this.parseMaybeJson(this.currentRecord.salesGroups, [])
this.editSalesItems = this.flattenSalesGroups(salesGroups)
this.productKeyword = ''
this.activeEditSalesIndex = -1
this.showEditPopup = true
await this.loadProductGroups()
},
closeEditPopup() {
this.showEditPopup = false
this.showProductPicker = false
this.activeEditSalesIndex = -1
this.productKeyword = ''
},
async openProductPicker(index) {
await this.loadProductGroups()
if (!this.productGroups.length) {
uni.showToast({
title: '当前连锁暂无可选商品',
icon: 'none'
})
return
}
this.activeEditSalesIndex = Number.isInteger(index) ? index : -1
this.productKeyword = ''
this.showProductPicker = true
if (!this.activeProductGroupKey && this.filteredProductGroups.length) {
this.activeProductGroupKey = this.filteredProductGroups[0].key
}
},
closeProductPicker() {
this.showProductPicker = false
this.activeEditSalesIndex = -1
},
selectProductGroup(key) {
this.activeProductGroupKey = key
},
isProductSelected(product) {
return (Array.isArray(this.editSalesItems) ? this.editSalesItems : []).some((item, index) => {
if (index === this.activeEditSalesIndex) return false
if (product.productId && item.productId) {
return String(item.productId) === String(product.productId)
}
return item.name === product.name
})
},
selectProduct(product) {
if (this.isProductSelected(product)) {
uni.showToast({
title: '该商品已添加',
icon: 'none'
})
return
}
const next = this.normalizeSalesGroupItem(product, product.groupTitle || '自定义商品')
if (this.activeEditSalesIndex >= 0 && this.editSalesItems[this.activeEditSalesIndex]) {
const current = this.editSalesItems[this.activeEditSalesIndex]
this.$set(this.editSalesItems, this.activeEditSalesIndex, Object.assign({}, current, next, {
qty: current.qty,
groupTitle: next.groupTitle || current.groupTitle
}))
} else {
next.qty = ''
this.editSalesItems.push(next)
}
this.closeProductPicker()
},
removeEditSalesItem(index) {
this.editSalesItems.splice(index, 1)
},
onEditQtyInput(event, index) {
const current = this.editSalesItems[index]
if (!current) return
const qty = event && event.detail ? event.detail.value : ''
this.$set(this.editSalesItems, index, Object.assign({}, current, {qty}))
},
onSpecChange(event, item) {
const value = Number(event.detail.value || 0)
item.spec = item.specOptions[value] || item.spec
},
parseImageList(list) {
const source = Array.isArray(list) ? list : []
return source
.map(item => {
if (!item) return ''
if (typeof item === 'string') return item
return item.url || item.path || item.image || ''
})
.filter(Boolean)
},
parseClockImageList(list) {
const source = Array.isArray(list) ? list : []
return source
.map(item => {
if (!item) return ''
if (typeof item === 'string') return item
return item.url || item.path || item.image || ''
})
.filter(Boolean)
},
parseMaybeJson(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
},
normalizeDetailData(data) {
const detail = data && typeof data === 'object' ? {...data} : {}
detail.clockPhotos = this.parseMaybeJson(detail.clockPhotos, [])
detail.boothPhotos = this.parseMaybeJson(detail.boothPhotos, [])
detail.receiptPhotos = this.parseImageList(this.parseMaybeJson(detail.receiptPhoto, []))
detail.salesGroups = this.parseMaybeJson(detail.salesGroups, [])
return detail
},
async saveEdit() {
if (this.editSaving) return
const items = Array.isArray(this.editSalesItems) ? this.editSalesItems : []
if (!items.length) {
uni.showToast({
title: '请至少保留一项销量商品',
icon: 'none'
})
return
}
for (let i = 0; i < items.length; i += 1) {
const item = items[i]
if (!String(item.name || '').trim()) {
uni.showToast({
title: '请选择商品',
icon: 'none'
})
return
}
if (this.parseNumber(item.qty) < 0) {
uni.showToast({
title: '数量不能小于0',
icon: 'none'
})
return
}
}
this.editSaving = true
try {
const salesGroups = this.buildSalesGroupsFromItems(items)
const reward = this.editTotalRewardAmount
await selfReportChangeReq({
id: this.currentRecord.id,
salesGroups: JSON.stringify(salesGroups),
reward
})
const nextRecord = Object.assign({}, this.currentRecord, {
salesGroups,
reward
})
this.detailData = nextRecord
this.detailLoaded = true
this.showEditPopup = false
this.showProductPicker = false
this.activeEditSalesIndex = -1
this.$emit('updated', nextRecord)
uni.showToast({
title: '保存成功',
icon: 'success'
})
} catch (error) {
uni.showToast({
title: error && error.message ? error.message : '保存失败',
icon: 'none'
})
} finally {
this.editSaving = false
}
},
formatPrice(value) {
const num = Number(value || 0)
return num.toFixed(2)
}
}
}
</script>
<style lang="scss" scoped>
@import './record.scss';
.record-section {
margin-top: 18rpx;
}
.record-meta-row {
display: flex;
align-items: center;
gap: 14rpx;
}
.record-meta-action {
margin-top: 6rpx;
color: #2fc6a7;
font-size: 24rpx;
white-space: nowrap;
}
.record-section-title {
font-size: 26rpx;
font-weight: 700;
color: #1f2937;
margin-bottom: 12rpx;
}
.record-toggle {
margin-top: 12rpx;
display: inline-flex;
align-items: center;
gap: 8rpx;
color: #2fc6a7;
font-size: 24rpx;
}
.detail-empty {
color: #94a3b8;
font-size: 24rpx;
}
.record-section-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
margin-bottom: 12rpx;
}
.record-section-title-row .record-section-title {
margin-bottom: 0;
}
.section-action {
color: #2fc6a7;
font-size: 24rpx;
white-space: nowrap;
}
.self-report-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 50;
background: rgba(15, 23, 42, 0.42);
}
.self-report-popup {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 20rpx;
z-index: 60;
max-height: 86vh;
overflow-y: auto;
padding: 20rpx;
border-radius: 18rpx;
background: #ffffff;
}
.popup-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.section-title {
font-size: 30rpx;
color: #1f2937;
font-weight: 700;
}
.close-btn {
color: #64748b;
font-size: 24rpx;
}
.edit-summary {
margin-top: 12rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 16rpx;
border-radius: 12rpx;
background: #f7f9fc;
}
.edit-summary-label {
color: #64748b;
font-size: 24rpx;
}
.edit-summary-value {
color: #f5576c;
font-size: 30rpx;
font-weight: 700;
}
.detail-actions {
margin-top: 12rpx;
}
.ghost-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10rpx 16rpx;
border-radius: 999rpx;
color: #2563eb;
background: #eff6ff;
font-size: 24rpx;
}
.edit-sales-list {
margin-top: 12rpx;
}
.edit-sales-item {
margin-bottom: 16rpx;
padding: 16rpx;
border-radius: 12rpx;
background: #f7f9fc;
}
.edit-sales-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12rpx;
margin-bottom: 10rpx;
}
.edit-sales-name {
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.edit-sales-group {
margin-top: 4rpx;
font-size: 22rpx;
color: #64748b;
}
.edit-sales-remove {
color: #f5576c;
font-size: 24rpx;
white-space: nowrap;
}
.form-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 10rpx;
}
.label {
width: 120rpx;
color: #64748b;
font-size: 24rpx;
flex-shrink: 0;
}
.picker-input,
.input {
flex: 1;
min-width: 0;
}
.input {
min-height: 72rpx;
padding: 0 20rpx;
border-radius: 12rpx;
background: #ffffff;
border: 1rpx solid #e5e7eb;
box-sizing: border-box;
}
.picker-input-inner {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 72rpx;
padding: 0 20rpx;
border-radius: 12rpx;
background: #ffffff;
border: 1rpx solid #e5e7eb;
box-sizing: border-box;
}
.picker-text {
color: #1f2937;
}
.picker-placeholder {
color: #94a3b8;
}
.bonus-text {
color: #f5576c;
font-weight: 700;
}
.qty-input-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.qty-input {
min-width: 0;
}
.qty-unit {
color: #64748b;
font-size: 24rpx;
white-space: nowrap;
}
.popup-actions {
margin-top: 16rpx;
}
.product-picker {
padding: 0;
}
.merchant-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 24rpx;
}
.picker-title {
font-size: 30rpx;
color: #1f2937;
font-weight: 700;
}
.picker-action {
color: #64748b;
font-size: 26rpx;
}
.picker-action.primary {
color: #2563eb;
}
.merchant-picker-search {
padding: 0 24rpx 18rpx;
}
.merchant-edit-picker {
background: #ffffff;
padding-bottom: calc(env(safe-area-inset-bottom) + 12rpx);
}
.merchant-picker-view {
width: 100%;
height: 420rpx;
}
.merchant-picker-item {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
padding: 0 24rpx;
text-align: center;
font-size: 26rpx;
color: #1f2937;
}
.merchant-search-input {
height: 72rpx;
padding: 0 24rpx;
border-radius: 999rpx;
background: #f7f9fc;
font-size: 26rpx;
}
.merchant-picker-loading {
min-height: 360rpx;
display: flex;
align-items: center;
justify-content: center;
}
.product-panel {
display: flex;
min-height: 540rpx;
}
.category-sidebar {
width: 180rpx;
max-height: 540rpx;
background: #f8fafc;
}
.category-item {
padding: 18rpx 14rpx;
font-size: 24rpx;
color: #475569;
}
.category-item-active {
color: #2563eb;
font-weight: 700;
background: #ffffff;
}
.product-panel-main {
flex: 1;
max-height: 540rpx;
padding: 0 18rpx 20rpx 12rpx;
}
.product-picker-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.product-picker-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 14rpx 12rpx;
border-radius: 12rpx;
background: #f8fafc;
}
.product-picker-main {
flex: 1;
min-width: 0;
}
.product-picker-name {
font-size: 26rpx;
font-weight: 700;
color: #1f2937;
}
.product-picker-sub,
.product-picker-income {
margin-top: 4rpx;
font-size: 22rpx;
color: #64748b;
}
.product-picker-income {
color: #f5576c;
}
.product-picker-tag {
color: #2563eb;
font-size: 24rpx;
white-space: nowrap;
}
</style>