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

1931 lines
67 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-title">{{ item.name }}</view>-->
<view class="record-meta">门店:{{ currentItem.merchantName }}</view>
<!-- <view class="record-meta">小票金额:¥{{ item.amount }}</view>-->
<!-- <view class="record-meta" style="color: red">-->
<!-- 红包:¥{{ currentItem.income ? currentItem.income : 0 }}-->
<!-- <text v-if="hasExtraIncome(currentItem)">(含额外加提:¥{{ formatExtraIncome(currentItem) }})</text>-->
<!-- </view>-->
<view class="record-meta">提交人:{{ currentItem.owner || item.owner }} · {{ currentItem.date || item.date }}</view>
<view class="record-meta">留言:{{ currentItem.remark || '-' }}</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">
<view v-if="currentItem.image" class="record-section">
<view class="record-section-title">小票查看</view>
<view class="media-images">
<image
class="media-image"
:src="currentItem.image"
mode="aspectFill"
@click="emitPreview(currentItem.image, [currentItem.image])"
/>
</view>
</view>
<view class="record-detail">
<view v-if="currentItem.detailList && currentItem.detailList.length" class="detail-list">
<view class="detail-item" v-for="(detail, idx) in currentItem.detailList" :key="idx">
<view class="detail-head">
<view class="detail-name">{{ detail.goodsName || detail.name || detail.title || '商品' }}</view>
</view>
<view class="detail-grid">
<view class="detail-kv">
<text class="detail-label">数量</text>
<text class="detail-value">x {{ detail.num }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">厂家</text>
<text class="detail-value">{{ resolveManufacturer(detail) }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">编号</text>
<text class="detail-value">{{ resolveCode(detail) }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">规格</text>
<text class="detail-value">{{ resolveSpec(detail) }}</text>
</view>
<view class="detail-kv">
<text class="detail-label">金额</text>
<text class="detail-value" style="color: red">{{ formatMoney(resolveAmount(detail)) }}</text>
</view>
<!-- <view class="detail-kv">-->
<!-- <text class="detail-label">额外加提</text>-->
<!-- <text class="detail-value" style="color: red">{{ formatMoney(resolveExtraAmount(detail)) }}</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', approveDisabled ? 'disabled-chip' : '']"
@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 class="flex-1 mx-15">
<view class="chip w-90p" @tap="openCopyPopup">抄送</view>
</view>
</view>
<view class="record-actions" v-if="detailExpanded && item.status === 2">
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="openCopyPopup">抄送</view>
</view>
</view>
</view>
<view v-if="showEditPopup" class="share-popup-mask" @tap="closeEditPopup" @touchmove.stop.prevent></view>
<view v-if="showEditPopup" class="share-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title"> 修改晒单</view>
<view class="close-btn" @tap="closeEditPopup">关闭</view>
</view>
<view class="form-row">
<view class="label">门店</view>
<view class="picker-input" @tap="openEditMerchantPicker">
<view class="picker-input-inner">
<text :class="editForm.merchantId ? 'picker-text' : 'picker-placeholder'">
{{ editForm.merchantId ? `${editForm.chainName || '-'}/${editForm.merchantName || '-'}` : '请选择连锁和门店' }}
</text>
<u-icon name="arrow-right" size="20" color="#9ca3af"/>
</view>
</view>
</view>
<view class="form-row">
<view class="label">订单号</view>
<input v-model="editForm.orderNo" class="input" placeholder="请输入订单号"/>
</view>
<view class="form-row">
<view class="label">日期</view>
<input v-model="editForm.date" class="input" placeholder="YYYY-MM-DD"/>
</view>
<view class="form-row">
<view class="label">小票金额</view>
<input v-model="editForm.amount" class="input" type="digit" placeholder="请输入小票金额"/>
</view>
<!-- <view class="form-row total-bonus-row">-->
<!-- <view class="label">总红包金额</view>-->
<!-- <view class="total-bonus-wrap">-->
<!-- <view class="form-value total-bonus-value">¥{{ editTotalBonusDisplay }}</view>-->
<!-- <view class="total-bonus-breakdown">-->
<!-- 红包金额:¥{{ editTotalIncomeBonusDisplay }},额外红包金额:¥{{ editTotalExtraBonusDisplay }}-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<view class="detail-title">商品明细</view>
<view class="detail-actions">
<view class="ghost-btn" @tap="addShareItem">新增商品</view>
</view>
<view v-if="editForm.list.length === 0" class="detail-empty">暂无明细</view>
<view v-else class="edit-detail-list">
<view class="edit-detail-item" v-for="(detail, idx) in editForm.list" :key="idx">
<view class="edit-detail-head">
<view class="edit-detail-index">第 {{ idx + 1 }} 项</view>
<view class="edit-detail-remove" @tap="removeShareItem(idx)">删除</view>
</view>
<view class="form-row">
<view class="label">药品</view>
<view class="picker-input" @tap="openEditProductPicker(idx)">
<view class="picker-input-inner">
<text :class="detail.goodsName ? 'picker-text' : 'picker-placeholder'">
{{ detail.goodsName || '点击选择药品' }}
</text>
<u-icon name="arrow-right" size="20" color="#9ca3af"/>
</view>
</view>
</view>
<view class="form-row">
<view class="label">编码</view>
<input
v-model="detail.goodsCode"
class="input"
placeholder="编码"
@input="onGoodsCodeInput($event, idx)"
@blur="onGoodsCodeBlur(idx)"
/>
</view>
<view class="form-row">
<view class="label">溯源码</view>
<input v-model="detail.sourceCode" class="input" placeholder="溯源码"/>
</view>
<!-- <view class="form-row">-->
<!-- <view class="label">红包金额</view>-->
<!-- <view class="form-value bonus-text">¥{{ resolveEditIncomeValue(detail).toFixed(2) }}</view>-->
<!-- </view>-->
<!-- <view class="form-row">-->
<!-- <view class="label">额外红包金额</view>-->
<!-- <view class="form-value bonus-text">¥{{ resolveEditExtraIncomeValue(detail).toFixed(2) }}</view>-->
<!-- </view>-->
<view class="form-row">
<view class="label">数量</view>
<input v-model="detail.num" class="input" type="number" placeholder="数量"/>
</view>
<view class="form-row">
<view class="label">规格</view>
<input v-model="detail.spec" class="input" placeholder="规格"/>
</view>
</view>
</view>
<view class="popup-actions">
<view
v-if="showRecognizeBtn"
class="ghost-action"
@tap="recognizeReceipt"
>{{ recognizing ? '识别中...' : '识别' }}
</view>
<view class="primary-btn submit" @tap="saveEdit">保存</view>
</view>
</view>
<view v-if="showEditMerchantPicker" class="share-popup-mask" @tap="closeEditMerchantPicker" @touchmove.stop.prevent></view>
<u-popup :show="showEditMerchantPicker" mode="bottom" round="24rpx" @close="closeEditMerchantPicker">
<view class="merchant-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeEditMerchantPicker">取消</view>
<text class="picker-title">选择连锁和门店</text>
<view class="picker-action primary" @tap="confirmEditMerchantPicker">确定</view>
</view>
<view class="merchant-picker-search">
<input
v-model="editMerchantSearchKeyword"
class="merchant-search-input"
placeholder="搜索连锁/门店"
placeholder-class="merchant-search-placeholder"
/>
</view>
<view v-if="merchantLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<picker-view
v-else
class="merchant-picker-view"
:value="editMerchantPickerValue"
:indicator-style="editMerchantIndicatorStyle"
@change="onEditMerchantPickerChange"
>
<picker-view-column>
<view
v-for="(item, idx) in editMerchantChainOptions"
:key="idx"
class="merchant-picker-item"
>{{ item.text }}</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(item, idx) in editMerchantStoreOptions"
:key="idx"
class="merchant-picker-item"
>{{ item.text }}</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
<u-popup :show="showEditProductPicker" mode="bottom" round="24rpx" @close="closeEditProductPicker">
<view class="product-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeEditProductPicker">取消</view>
<text class="picker-title">选择药品</text>
<view class="picker-action primary" @tap="closeEditProductPicker">完成</view>
</view>
<view class="merchant-picker-search">
<input
v-model="editProductKeyword"
class="merchant-search-input"
placeholder="搜索药品名称/编码/厂家/规格"
placeholder-class="merchant-search-placeholder"
/>
</view>
<view v-if="editProductLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<scroll-view v-else class="product-scroll" scroll-y>
<view v-if="filteredEditProductList.length === 0" class="detail-empty">暂无药品</view>
<view
v-for="product in filteredEditProductList"
:key="product._key"
class="product-option"
@tap="selectEditProduct(product)"
>
<view class="product-option-name">{{ product.goodsName || '-' }}</view>
<view class="product-option-meta">厂家:{{ product.makerAbb || '-' }}</view>
<view class="product-option-meta">规格:{{ product.spec || '-' }}</view>
<view class="product-option-meta">编码:{{ product.goodsCode || '-' }}</view>
<!-- <view class="product-option-meta">红包金额:¥{{ formatMoneyAmount(getEffectiveIncome(product)) }}</view>-->
<!-- <view class="product-option-meta">额外红包金额:¥{{ formatMoneyAmount(getEffectiveExtraIncome(product)) }}</view>-->
</view>
</scroll-view>
</view>
</u-popup>
<view v-if="showCopyPopup" class="share-popup-mask" @tap="closeCopyPopup" @touchmove.stop.prevent></view>
<view v-if="showCopyPopup" class="share-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title">抄送</view>
<view class="close-btn" @tap="closeCopyPopup">关闭</view>
</view>
<view class="copy-search-row">
<input
v-model="copyKeyword"
class="copy-search-input"
type="text"
placeholder="输入姓名/手机号搜索用户"
@confirm="searchCopyUsers"
/>
<view class="copy-search-btn" @tap="searchCopyUsers">搜索</view>
</view>
<view v-if="copySearching" class="copy-loading">搜索中...</view>
<scroll-view v-else class="copy-user-list" scroll-y>
<view
v-for="user in copyUsers"
:key="`copy-share-user-${user.id}`"
:class="['copy-user-item', selectedCopyUserId === user.id ? 'copy-user-item-active' : '']"
@tap="selectedCopyUserId = user.id"
>
<text class="copy-user-name">{{ user.label }}</text>
<text class="copy-user-phone">{{ user.phone || '' }}</text>
</view>
<view v-if="copyUsers.length === 0" class="copy-empty">暂无用户</view>
</scroll-view>
<view class="primary-btn submit" @tap="submitCopy">确认抄送</view>
</view>
</view>
</template>
<script>
import dayjs from 'dayjs'
import {productByCodeReq, shareAuditDetailReq, shareCopyReq} from "@/api/share";
import {merchantGroupByChainReq} from "@/api/shop";
import {aiReq} from "@/api/common";
import {userSearchReq} from "@/api/user";
import {productListReq} from "@/api/product";
export default {
name: 'AuditShareItem',
props: {
item: {
type: Object,
required: true
}
},
data() {
return {
showEditPopup: false,
showEditMerchantPicker: false,
showEditProductPicker: false,
merchantLoading: false,
editProductLoading: false,
merchantChainList: [],
editProductList: [],
editProductKeyword: '',
activeEditProductRowIndex: -1,
editForm: {
chainName: '',
chainId: '',
merchantName: '',
merchantId: '',
orderNo: '',
date: '',
amount: '',
list: []
},
recognizing: false,
goodsCodeTimers: {},
editMerchantSearchKeyword: '',
editMerchantPickerValue: [0, 0],
detailExpanded: false,
detailLoading: false,
detailLoaded: false,
detailData: null,
showCopyPopup: false,
copyKeyword: '',
copyUsers: [],
copySearching: false,
selectedCopyUserId: ''
}
},
computed: {
approveDisabled() {
const total = this.calcTotalIncome(this.currentItem.detailList)
return !total || total <= 0
},
currentItem() {
return this.detailData || this.item
},
editMerchantChainOptions() {
const list = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
const keyword = (this.editMerchantSearchKeyword || '').trim()
if (!keyword) return list.length ? list : [{id: '', text: '暂无连锁', merchants: []}]
const filtered = list
.map(chain => {
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
const chainMatch = (chain.text || '').includes(keyword)
if (chainMatch) return {...chain}
const matchedMerchants = merchants.filter(merchant => (merchant.text || '').includes(keyword))
if (!matchedMerchants.length) return null
return {
...chain,
merchants: matchedMerchants
}
})
.filter(Boolean)
return filtered.length ? filtered : [{id: '', text: '无匹配连锁', merchants: []}]
},
editMerchantStoreOptions() {
const chainIndex = Number(this.editMerchantPickerValue[0] || 0)
const currentChain = this.editMerchantChainOptions[chainIndex] || this.editMerchantChainOptions[0] || {}
const merchants = Array.isArray(currentChain.merchants) ? currentChain.merchants : []
return merchants.length ? merchants : [{id: '', text: '无门店'}]
},
editMerchantPickerIndexes() {
const chainList = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
if (!chainList.length) return [0, 0]
let chainIndex = chainList.findIndex(item => item.id === this.editForm.chainId)
if (chainIndex < 0 && this.editForm.merchantId) {
chainIndex = chainList.findIndex(item => {
const merchants = Array.isArray(item.merchants) ? item.merchants : []
return merchants.some(merchant => merchant.id === this.editForm.merchantId)
})
}
if (chainIndex < 0) chainIndex = 0
const merchants = Array.isArray(chainList[chainIndex].merchants) ? chainList[chainIndex].merchants : []
let merchantIndex = merchants.findIndex(item => item.id === this.editForm.merchantId)
if (merchantIndex < 0) merchantIndex = 0
return [chainIndex, merchantIndex]
},
editMerchantIndicatorStyle() {
return 'height: 64px;'
},
filteredEditProductList() {
const list = Array.isArray(this.editProductList) ? this.editProductList : []
const keyword = (this.editProductKeyword || '').trim().toLowerCase()
if (!keyword) return list
return list.filter(item => {
const name = (item.goodsName || '').toLowerCase()
const code = (item.goodsCode || '').toLowerCase()
const maker = (item.makerAbb || '').toLowerCase()
const spec = (item.spec || '').toLowerCase()
return name.includes(keyword) || code.includes(keyword) || maker.includes(keyword) || spec.includes(keyword)
})
},
editSelectedProductRows() {
const list = Array.isArray(this.editForm.list) ? this.editForm.list : []
return list
.map(item => {
const num = this.parseQuantity(item.num)
const validNum = Number.isFinite(num) && num > 0 ? num : 0
const income = this.resolveEditIncomeValue(item)
const extraIncome = this.resolveEditExtraIncomeValue(item)
const singleBonus = income + extraIncome
return {
...item,
_num: validNum || NaN,
_baseBonusAmount: income * validNum,
_extraBonusAmount: extraIncome * validNum,
_bonusAmount: singleBonus * validNum
}
})
.filter(item => Number.isFinite(item._num) && item._num > 0)
},
editTotalIncomeBonus() {
return this.editSelectedProductRows.reduce((sum, item) => sum + (Number(item._baseBonusAmount) || 0), 0)
},
editTotalExtraBonus() {
return this.editSelectedProductRows.reduce((sum, item) => sum + (Number(item._extraBonusAmount) || 0), 0)
},
editTotalBonus() {
return this.editSelectedProductRows.reduce((sum, item) => sum + (Number(item._bonusAmount) || 0), 0)
},
editTotalIncomeBonusDisplay() {
return this.editTotalIncomeBonus.toFixed(2)
},
editTotalExtraBonusDisplay() {
return this.editTotalExtraBonus.toFixed(2)
},
editTotalBonusDisplay() {
return this.editTotalBonus.toFixed(2)
},
showRecognizeBtn() {
const amount = this.editForm.amount
return amount === null || amount === undefined || String(amount).trim() === ''
}
},
watch: {
editMerchantSearchKeyword() {
this.editMerchantPickerValue = [0, 0]
}
},
beforeDestroy() {
this.clearGoodsCodeTimers()
},
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 shareAuditDetailReq(this.item.id)
this.detailData = this.normalizeDetailResponse(data)
this.detailLoaded = true
} catch (error) {
uni.showToast({
title: '详情加载失败',
icon: 'none'
})
} finally {
this.detailLoading = false
}
},
emitPreview(url, list) {
this.$emit('preview', url, list)
},
clearGoodsCodeTimers() {
const keys = Object.keys(this.goodsCodeTimers || {})
keys.forEach(key => {
clearTimeout(this.goodsCodeTimers[key])
})
this.goodsCodeTimers = {}
},
normalizeNumericText(value) {
return String(value || '')
.trim()
.replace(/[0-9]/g, char => String.fromCharCode(char.charCodeAt(0) - 65248))
.replace(/,/g, ',')
.replace(/。/g, '.')
.replace(/,/g, '')
},
parseQuantity(value) {
const text = this.normalizeNumericText(value)
if (!text) return NaN
const num = Number(text)
return Number.isFinite(num) ? num : NaN
},
normalizeMatchValue(value) {
if (value === undefined || value === null) return ''
return String(value).trim().toLowerCase()
},
findMatchedDetailFromItemDetailList(row = {}, product = {}) {
const sourceItem = this.currentItem
const list = Array.isArray(sourceItem && sourceItem.detailList) ? sourceItem.detailList : []
if (!list.length) return null
const targetId = this.normalizeMatchValue(product.productId || row.productId)
const targetCode = this.normalizeMatchValue(product.goodsCode || row.goodsCode)
const targetName = this.normalizeMatchValue(product.goodsName || row.goodsName)
for (let i = 0; i < list.length; i++) {
const detail = list[i] || {}
const detailProduct = detail.product || {}
const detailId = this.normalizeMatchValue(
detail.productId
|| detail.goodsId
|| detail.id
|| detail.skuId
|| detailProduct.productId
|| detailProduct.goodsId
|| detailProduct.id
|| detailProduct.skuId
)
if (targetId && detailId && targetId === detailId) return detail
const detailCode = this.normalizeMatchValue(
detail.goodsCode
|| detail.code
|| detail.productCode
|| detail.skuCode
|| detail.batchCode
|| detailProduct.goodsCode
|| detailProduct.code
|| detailProduct.productCode
|| detailProduct.skuCode
|| detailProduct.batchCode
)
if (targetCode && detailCode && targetCode === detailCode) return detail
const detailName = this.normalizeMatchValue(
detail.goodsName
|| detail.name
|| detail.title
|| detailProduct.goodsName
|| detailProduct.name
|| detailProduct.title
)
if (targetName && detailName && targetName === detailName) return detail
}
return null
},
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 ''
},
pickFirstNumber(values = [], fallback = 0) {
for (let i = 0; i < values.length; i++) {
const raw = values[i]
if (raw === '' || raw === null || raw === undefined) continue
const num = Number(raw)
if (Number.isFinite(num)) return num
}
return fallback
},
normalizeExtraIncomeList(rawValue) {
const parseText = text => {
try {
return JSON.parse(text)
} catch (err) {
try {
const normalized = text
.replace(/^'/, '"')
.replace(/'$/, '"')
.replace(/([{,]\s*)'([^']+?)'\s*:/g, '$1"$2":')
.replace(/:\s*'([^']*?)'(\s*[},])/g, ':"$1"$2')
return JSON.parse(normalized)
} catch (error) {
return null
}
}
}
const objectToList = obj => {
if (!obj || typeof obj !== 'object') return []
const wrapped = this.unwrapNestedList(obj)
if (Array.isArray(wrapped) && wrapped.length && wrapped[0] !== obj) return wrapped
if (Array.isArray(obj.list)) return obj.list
const values = Object.keys(obj)
.sort((a, b) => Number(a) - Number(b))
.map(key => obj[key])
const objectValues = values.filter(val => val && typeof val === 'object')
if (objectValues.length) return objectValues
if (obj.startTime || obj.start_time || obj.extraStartTime || obj.extra_start_time || obj.income !== undefined) {
return [obj]
}
return []
}
let current = rawValue
for (let i = 0; i < 3; i++) {
if (Array.isArray(current)) return current
if (current === undefined || current === null) return []
if (typeof current === 'object') return objectToList(current)
const text = String(current).trim()
if (!text) return []
const parsed = parseText(text)
if (parsed === null) return []
current = parsed
}
return []
},
normalizeExtraIncomeItem(item) {
if (!item || typeof item !== 'object') {
return {
income: 0,
startTime: '',
endTime: ''
}
}
const incomeRaw = item.income ?? item.extraIncome ?? item.amount ?? item.reward ?? item.bonus ?? item.value ?? 0
const startTime = item.startTime ?? item.start_time ?? item.extraStartTime ?? item.extra_start_time ?? item.beginTime ?? item.begin_time ?? ''
const endTime = item.endTime ?? item.end_time ?? item.extraEndTime ?? item.extra_end_time ?? item.finishTime ?? item.finish_time ?? ''
const income = Number(incomeRaw)
return {
income: Number.isFinite(income) ? income : 0,
startTime,
endTime
}
},
hasPeriod(startTime, endTime) {
const hasStart = startTime !== undefined && startTime !== null && String(startTime).trim() !== ''
const hasEnd = endTime !== undefined && endTime !== null && String(endTime).trim() !== ''
return hasStart && hasEnd
},
parsePeriodToDayjs(value) {
if (value === undefined || value === null) return null
const raw = String(value).trim()
if (!raw) return null
if (/^-?\d+(\.\d+)?$/.test(raw)) {
const num = Number(raw)
if (!Number.isFinite(num)) return null
const millis = Math.abs(num) < 1e12 ? num * 1000 : num
const parsedNum = dayjs(millis)
return parsedNum.isValid() ? parsedNum : null
}
const parsedText = dayjs(raw)
return parsedText.isValid() ? parsedText : null
},
isCurrentInPeriod(startTime, endTime) {
if (!this.hasPeriod(startTime, endTime)) return true
const start = this.parsePeriodToDayjs(startTime)
const end = this.parsePeriodToDayjs(endTime)
if (!start || !end) return true
const now = dayjs()
return (now.isAfter(start) || now.isSame(start)) && (now.isBefore(end) || now.isSame(end))
},
isCurrentInPeriodStrict(startTime, endTime) {
if (!this.hasPeriod(startTime, endTime)) return false
const start = this.parsePeriodToDayjs(startTime)
const end = this.parsePeriodToDayjs(endTime)
if (!start || !end) return false
const now = dayjs()
return (now.isAfter(start) || now.isSame(start)) && (now.isBefore(end) || now.isSame(end))
},
getExtraIncomeDisplayPeriods(detail = {}) {
const list = this.normalizeExtraIncomeList(detail.extraIncomeList)
.map(item => this.normalizeExtraIncomeItem(item))
.filter(item => this.hasPeriod(item.startTime, item.endTime))
if (list.length) return list
if (!this.hasPeriod(detail.extraStartTime, detail.extraEndTime)) return []
return [{
startTime: detail.extraStartTime,
endTime: detail.extraEndTime,
income: Number(detail.extraIncome) || 0
}]
},
getExtraIncomePeriods(detail = {}) {
const list = this.getExtraIncomeDisplayPeriods(detail)
.filter(item => (Number(item.income) || 0) > 0)
if (list.length) return list
return []
},
getEffectiveIncome(detail = {}) {
const amount = Number(detail.income)
const base = Number.isFinite(amount) && amount > 0 ? amount : 0
return this.isCurrentInPeriod(detail.startTime, detail.endTime) ? base : 0
},
getEffectiveExtraIncome(detail = {}) {
const periods = this.getExtraIncomePeriods(detail)
if (!periods.length) return 0
return periods.reduce((sum, item) => {
if (!this.isCurrentInPeriodStrict(item.startTime, item.endTime)) return sum
return sum + (Number(item.income) || 0)
}, 0)
},
formatMoneyAmount(value) {
const num = Number(value)
return Number.isFinite(num) ? num.toFixed(2) : '0.00'
},
unwrapNestedList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList', 'children', 'productList', 'products']
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.unwrapNestedList(value)
if (nested.length) return nested
}
}
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 = ['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
}, [])
},
normalizePickerProduct(item = {}) {
const product = item && item.product ? item.product : {}
const pickValue = (...values) => {
for (let i = 0; i < values.length; i++) {
const value = values[i]
if (value !== undefined && value !== null && String(value).trim() !== '') {
return value
}
}
return ''
}
const goodsCode = pickValue(item.goodsCode, item.code, item.productCode, item.batchCode, product.code, product.goodsCode, product.batchCode)
const productId = pickValue(item.productId, item.id, item.goodsId, item.skuId, product.productId, product.id, product.goodsId, product.skuId)
const goodsName = pickValue(item.goodsName, item.goods_name, item.title, item.name, product.title, product.goodsName, product.name)
const makerAbb = pickValue(item.makerAbb, item.maker, item.manufacturer, product.makerAbb, product.maker, product.manufacturer)
const spec = pickValue(product.spec, item.spec, item.specification)
const merchantId = pickValue(item.merchantId, item.merchant_id, product.merchantId, product.merchant_id)
const merchantName = pickValue(item.merchantName, item.merchant_name, item.shopName, item.shop_name, product.merchantName, product.merchant_name)
const chainId = pickValue(item.chainId, item.chain_id, product.chainId, product.chain_id)
const chainName = pickValue(item.chainName, item.chain_name, item.chain, product.chainName, product.chain_name, product.chain)
const incomeRaw = product.income !== undefined && product.income !== null
? product.income
: (item.income !== undefined && item.income !== null ? item.income : (item.shareIncome || item.reward || 0))
const extraIncomeRaw = product.extraIncome !== undefined && product.extraIncome !== null
? product.extraIncome
: (item.extraIncome !== undefined && item.extraIncome !== null ? item.extraIncome : (item.extraReward || item.extraBonus || item.otherIncome || 0))
const extraIncomeListRaw = pickValue(
item.extraIncomeList,
item.extra_income_list,
product.extraIncomeList,
product.extra_income_list
)
const startTime = pickValue(item.startTime, item.start_time, product.startTime, product.start_time)
const endTime = pickValue(item.endTime, item.end_time, product.endTime, product.end_time)
const extraStartTime = pickValue(item.extraStartTime, item.extra_start_time, product.extraStartTime, product.extra_start_time)
const extraEndTime = pickValue(item.extraEndTime, item.extra_end_time, product.extraEndTime, product.extra_end_time)
return {
productId,
goodsCode,
goodsName,
makerAbb,
spec,
merchantId,
merchantName,
chainId,
chainName,
income: Number(incomeRaw) || 0,
extraIncome: Number(extraIncomeRaw) || 0,
extraIncomeList: this.normalizeExtraIncomeList(extraIncomeListRaw),
startTime,
endTime,
extraStartTime,
extraEndTime
}
},
normalizeEditDetail(detail = {}) {
const product = this.normalizePickerProduct(detail.product || detail)
return {
productId: detail.productId || detail.goodsId || product.productId || '',
goodsCode: detail.goodsCode || detail.code || product.goodsCode || '',
sourceCode: detail.sourceCode || detail.source_code || '',
goodsName: detail.goodsName || detail.name || detail.title || product.goodsName || '',
num: detail.num || detail.qty || '',
spec: detail.spec || detail.specification || product.spec || '',
makerAbb: detail.makerAbb || detail.manufacturer || product.makerAbb || '',
amount: detail.amount || '',
income: this.pickFirstNumber([detail.income, detail.productIncome, detail.bonus, product.income], 0),
extraIncome: this.pickFirstNumber([detail.extraIncome, detail.extra_bonus, product.extraIncome], 0),
incomeSingle: detail.incomeSingle ?? detail.income_single ?? '',
extraIncomeSingle: detail.extraIncomeSingle ?? detail.extra_income_single ?? '',
extraIncomeList: detail.extraIncomeList || detail.extra_income_list || product.extraIncomeList || [],
startTime: detail.startTime || detail.start_time || product.startTime || '',
endTime: detail.endTime || detail.end_time || product.endTime || '',
extraStartTime: detail.extraStartTime || detail.extra_start_time || product.extraStartTime || '',
extraEndTime: detail.extraEndTime || detail.extra_end_time || product.extraEndTime || '',
product,
_preferSingleIncomeDisplay: true
}
},
buildEditForm(item) {
const sourceItem = this.currentItem
const list = Array.isArray(sourceItem.detailList) ? sourceItem.detailList : []
return {
chainName: sourceItem.chainName || sourceItem.chain_name || '',
chainId: sourceItem.chainId || sourceItem.chain_id || sourceItem.chainIdNormalized || '',
merchantName: sourceItem.merchantName || '',
merchantId: sourceItem.merchantId || sourceItem.merchant_id || '',
orderNo: sourceItem.orderNo || '',
date: sourceItem.date || '',
amount: sourceItem.amount || '',
list: list.map(detail => this.normalizeEditDetail(detail))
}
},
openEditPopup() {
if (this.item.status !== 0) {
return
}
this.editForm = this.buildEditForm(this.item)
this.editMerchantSearchKeyword = ''
this.editMerchantPickerValue = [...this.editMerchantPickerIndexes]
this.editProductKeyword = ''
this.editProductList = []
this.activeEditProductRowIndex = -1
this.showEditPopup = true
},
closeEditPopup() {
this.showEditPopup = false
this.showEditProductPicker = false
this.editProductList = []
this.activeEditProductRowIndex = -1
this.clearGoodsCodeTimers()
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result']
for (let i = 0; i < keys.length; i++) {
if (Array.isArray(data[keys[i]])) return data[keys[i]]
}
return []
},
normalizeUser(item) {
const id = item.id ?? item.userId ?? item.uid
const nickname = item.nickname || item.nickName || ''
const realName = item.realName || item.real_name || item.name || ''
const phone = item.phone || item.mobile || item.tel || ''
const label = realName || nickname || phone || `用户${id || ''}`
return {
id,
label,
phone
}
},
openCopyPopup() {
this.copyKeyword = ''
this.copyUsers = []
this.selectedCopyUserId = ''
this.showCopyPopup = true
},
closeCopyPopup() {
this.showCopyPopup = false
},
async searchCopyUsers() {
const keyword = (this.copyKeyword || '').trim()
if (!keyword) {
uni.showToast({
title: '请输入keyword',
icon: 'none'
})
return
}
this.copySearching = true
try {
const {data} = await userSearchReq({
keywords: keyword
})
const list = this.unwrapList(data)
this.copyUsers = list
.map(item => this.normalizeUser(item))
.filter(item => item.id !== undefined && item.id !== null && String(item.id).trim() !== '')
this.selectedCopyUserId = this.copyUsers[0] ? this.copyUsers[0].id : ''
} catch (error) {
this.copyUsers = []
uni.showToast({
title: '搜索失败',
icon: 'none'
})
} finally {
this.copySearching = false
}
},
async submitCopy() {
const targetUserId = this.selectedCopyUserId
if (targetUserId === '' || targetUserId === null || targetUserId === undefined) {
uni.showToast({
title: '请选择用户',
icon: 'none'
})
return
}
try {
await shareCopyReq({
id: this.item.id,
sourceUserId: this.item.userId,
userId: targetUserId,
isCopy: 1
})
uni.showToast({
title: '抄送成功',
icon: 'success'
})
this.closeCopyPopup()
this.$emit('copied')
} catch (error) {
uni.showToast({
title: '抄送失败',
icon: 'none'
})
}
},
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, raw: item}
},
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 ?? chainName
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
}
}
let merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(child => child.id !== undefined && child.id !== null && String(child.id).trim() !== '')
if (!merchants.length) {
const selfMerchant = this.normalizeMerchant(item)
if (selfMerchant.id !== undefined && selfMerchant.id !== null && String(selfMerchant.id).trim() !== '') {
merchants = [selfMerchant]
}
}
return {
id: chainId,
text: chainName,
merchants
}
},
normalizeChainIdForApi(rawChainId) {
const text = String(rawChainId || '').trim()
if (!text || text.startsWith('chain_')) return ''
return text
},
getArrayFromResponse(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result']
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.getArrayFromResponse(value)
if (nested.length) return nested
}
}
const values = Object.keys(data).map(key => data[key])
for (let i = 0; i < values.length; i++) {
if (Array.isArray(values[i])) return values[i]
}
return []
},
uniquePickerProducts(list = []) {
const map = {}
const rows = []
list.forEach(item => {
const key = item.productId || item.goodsCode || `${item.goodsName || ''}_${item.spec || ''}_${item.makerAbb || ''}`
const id = String(key || '').trim()
if (!id || map[id]) return
map[id] = true
rows.push(item)
})
return rows
},
matchesCurrentMerchantChain(product = {}) {
const selectedMerchantId = String(this.editForm.merchantId || '').trim()
const selectedChainId = this.normalizeChainIdForApi(this.editForm.chainId)
if (selectedMerchantId) {
const productMerchantId = String(product.merchantId || '').trim()
if (productMerchantId && productMerchantId !== selectedMerchantId) return false
}
if (selectedChainId) {
const productChainId = String(product.chainId || '').trim()
if (productChainId && productChainId !== selectedChainId) return false
}
return true
},
async getMerchantList() {
this.merchantLoading = true
try {
const {data} = await merchantGroupByChainReq({})
const list = this.unwrapNestedList(data)
this.merchantChainList = list
.map((item, index) => this.normalizeMerchantChain(item, index))
.filter(item => Array.isArray(item.merchants) && item.merchants.length > 0)
} catch (error) {
this.merchantChainList = []
uni.showToast({
title: '门店列表获取失败',
icon: 'none'
})
} finally {
this.merchantLoading = false
}
},
async openEditMerchantPicker() {
if (!this.merchantChainList || this.merchantChainList.length === 0) {
await this.getMerchantList()
}
this.editMerchantPickerValue = [...this.editMerchantPickerIndexes]
this.showEditMerchantPicker = true
},
closeEditMerchantPicker() {
this.showEditMerchantPicker = false
},
confirmEditMerchantPicker() {
const chainIndex = Number(this.editMerchantPickerValue[0] || 0)
const merchantIndex = Number(this.editMerchantPickerValue[1] || 0)
const chain = this.editMerchantChainOptions[chainIndex] || null
const merchants = chain && Array.isArray(chain.merchants) ? chain.merchants : []
const selected = merchants[merchantIndex]
if (!selected || selected.id === '' || selected.id === null || selected.id === undefined) {
return
}
this.editForm.chainName = (chain && chain.text) || ''
this.editForm.chainId = (chain && chain.id) || ''
this.editForm.merchantName = selected.text || ''
this.editForm.merchantId = selected.id ?? ''
this.editProductList = []
this.editProductKeyword = ''
this.showEditMerchantPicker = false
},
onEditMerchantPickerChange(event) {
const value = event && event.detail ? event.detail.value : null
const currentChainIndex = Number(this.editMerchantPickerValue[0] || 0)
let chainIndex = Array.isArray(value) ? Number(value[0] || 0) : Number(value || 0)
let merchantIndex = Array.isArray(value) ? Number(value[1] || 0) : 0
if (!Number.isFinite(chainIndex) || chainIndex < 0) chainIndex = 0
if (!Number.isFinite(merchantIndex) || merchantIndex < 0) merchantIndex = 0
const chain = this.editMerchantChainOptions[chainIndex] || this.editMerchantChainOptions[0] || {}
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
if (!merchants.length) {
merchantIndex = 0
} else if (chainIndex !== currentChainIndex || merchantIndex > merchants.length - 1) {
merchantIndex = 0
}
this.editMerchantPickerValue = [chainIndex, merchantIndex]
},
async openEditProductPicker(index) {
if (index < 0 || index >= this.editForm.list.length) return
if (!this.editForm.merchantId && !this.normalizeChainIdForApi(this.editForm.chainId)) {
uni.showToast({
title: '请先选择连锁和门店',
icon: 'none'
})
return
}
this.activeEditProductRowIndex = index
this.editProductKeyword = ''
await this.getEditProductList()
this.showEditProductPicker = true
},
closeEditProductPicker() {
this.showEditProductPicker = false
},
async getEditProductList() {
this.editProductLoading = true
try {
const {data} = await productListReq()
const rawList = this.getArrayFromResponse(data)
const normalizedAll = rawList
.map(item => this.normalizePickerProduct(item))
.filter(item => !!item.goodsName)
const filtered = normalizedAll.filter(item => this.matchesCurrentMerchantChain(item))
const resolvedRows = filtered.length ? filtered : normalizedAll
const uniqueRows = this.uniquePickerProducts(resolvedRows)
this.editProductList = uniqueRows.map((item, index) => ({
...item,
_key: `edit-product-${index}`
}))
} catch (error) {
this.editProductList = []
uni.showToast({
title: '药品数据获取失败',
icon: 'none'
})
} finally {
this.editProductLoading = false
}
},
applyProductToEditDetail(index, rawProduct, fallbackCode = '') {
const row = this.editForm.list[index]
if (!row) return
const product = this.normalizePickerProduct(rawProduct || {})
const matchedDetail = this.findMatchedDetailFromItemDetailList(row, product)
const matchedIncome = matchedDetail
? this.pickFirstNumber([matchedDetail.extraIncome, matchedDetail.productIncome, matchedDetail.bonus], 0)
: null
this.$set(row, 'product', product)
this.$set(row, 'productId', product.productId || row.productId || '')
this.$set(row, 'goodsCode', product.goodsCode || fallbackCode || row.goodsCode || '')
this.$set(row, 'goodsName', product.goodsName || row.goodsName || '')
this.$set(row, 'makerAbb', product.makerAbb || row.makerAbb || '')
this.$set(row, 'spec', product.spec || row.spec || '')
this.$set(row, 'income', Number(product.income) || 0)
this.$set(row, 'extraIncome', rawProduct.extraIncome || 0)
this.$set(row, 'incomeSingle', '')
this.$set(row, 'extraIncomeSingle', '')
this.$set(row, 'extraIncomeList', Array.isArray(product.extraIncomeList) ? product.extraIncomeList : [])
this.$set(row, 'startTime', product.startTime || '')
this.$set(row, 'endTime', product.endTime || '')
this.$set(row, 'extraStartTime', product.extraStartTime || '')
this.$set(row, 'extraEndTime', product.extraEndTime || '')
this.$set(row, '_preferSingleIncomeDisplay', false)
},
clearProductInfo(index) {
const row = this.editForm.list[index]
if (!row) return
this.$set(row, 'product', null)
this.$set(row, 'productId', '')
this.$set(row, 'goodsName', '')
this.$set(row, 'makerAbb', '')
this.$set(row, 'spec', '')
this.$set(row, 'income', 0)
this.$set(row, 'extraIncome', 0)
this.$set(row, 'incomeSingle', '')
this.$set(row, 'extraIncomeSingle', '')
this.$set(row, 'extraIncomeList', [])
this.$set(row, 'startTime', '')
this.$set(row, 'endTime', '')
this.$set(row, 'extraStartTime', '')
this.$set(row, 'extraEndTime', '')
this.$set(row, '_preferSingleIncomeDisplay', false)
},
selectEditProduct(product) {
const index = this.activeEditProductRowIndex
if (index < 0 || index >= this.editForm.list.length) return
this.applyProductToEditDetail(index, product)
this.showEditProductPicker = false
},
addShareItem() {
this.editForm.list.push({
productId: '',
goodsCode: '',
goodsName: '',
num: '',
makerAbb: '',
spec: '',
amount: '',
sourceCode: '',
income: 0,
extraIncome: 0,
incomeSingle: '',
extraIncomeSingle: '',
extraIncomeList: [],
startTime: '',
endTime: '',
extraStartTime: '',
extraEndTime: '',
product: null,
_preferSingleIncomeDisplay: false
})
},
removeShareItem(idx) {
this.editForm.list.splice(idx, 1)
},
onGoodsCodeInput(event, index) {
const fallback = this.editForm.list[index] ? this.editForm.list[index].goodsCode : ''
const value = event && event.detail && event.detail.value !== undefined ? event.detail.value : fallback
this.queueGoodsCodeLookup(index, value)
},
onGoodsCodeBlur(index) {
const item = this.editForm.list[index]
const code = item ? item.goodsCode : ''
this.fetchProductByCode(index, code)
},
async queueGoodsCodeLookup(index, code) {
if (this.goodsCodeTimers[index]) {
clearTimeout(this.goodsCodeTimers[index])
}
const trimmed = (code || '').trim()
if (!trimmed) {
if (this.editForm.list[index]) {
this.clearProductInfo(index)
}
return
}
this.goodsCodeTimers[index] = setTimeout(async () => {
await this.fetchProductByCode(index, trimmed)
}, 400)
},
async fetchProductByCode(index, code) {
if (!this.editForm.merchantName) {
return uni.showToast({
title: '请填写门店名称',
icon: 'none'
})
}
const trimmed = (code || '').trim()
if (!trimmed) {
if (this.editForm.list[index]) {
this.clearProductInfo(index)
}
return
}
try {
const {data} = await productByCodeReq(trimmed, this.editForm.merchantName)
if (this.editForm.list[index]) {
this.applyProductToEditDetail(index, data || {}, trimmed)
}
} catch (err) {
if (this.editForm.list[index]) {
this.clearProductInfo(index)
this.$set(this.editForm.list[index], 'goodsCode', trimmed)
}
}
},
saveEdit() {
const detailList = this.editForm.list.map(item => ({
goodsId: item.productId || '',
goodsCode: item.goodsCode || '',
goodsName: item.goodsName || '',
num: item.num || '',
spec: item.spec || '',
amount: item.amount || '',
sourceCode: item.sourceCode || '',
income: this.resolveEditIncomeValue(item),
extraIncome: this.resolveEditExtraIncomeValue(item)
}))
const payload = {
id: this.item.id,
chainId: this.editForm.chainId || this.item.chainId || '',
chainName: this.editForm.chainName || this.item.chainName || '',
merchantName: this.editForm.merchantName,
orderNo: this.editForm.orderNo,
date: this.editForm.date,
detailList,
amount: this.editForm.amount,
income: Number(this.editTotalBonusDisplay) || 0,
extraIncome: Number(this.editTotalExtraBonusDisplay) || 0,
merchantId: this.editForm.merchantId || this.item.merchantId || '',
userId: this.item.userId,
status: 0,
}
this.$emit('update', {
item: this.item,
payload,
localDetailList: this.editForm.list
})
this.showEditPopup = false
},
parseAiJson(value) {
if (value === null || value === undefined) return null
if (typeof value === 'object') return value
let text = String(value).trim()
if (!text) return null
text = text.replace(/^```[a-zA-Z]*\n?/, '').replace(/```$/g, '').trim()
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
text = text.slice(1, -1).trim()
}
const objStart = text.indexOf('{')
const objEnd = text.lastIndexOf('}')
const arrStart = text.indexOf('[')
const arrEnd = text.lastIndexOf(']')
if (objStart !== -1 && objEnd > objStart) {
text = text.slice(objStart, objEnd + 1)
} else if (arrStart !== -1 && arrEnd > arrStart) {
text = text.slice(arrStart, arrEnd + 1)
}
try {
return JSON.parse(text)
} catch (err) {
const normalized = text.replace(/'/g, '"')
try {
return JSON.parse(normalized)
} catch (error) {
return null
}
}
},
normalizeAiField(value) {
if (value === null || value === undefined) return ''
return String(value).trim()
},
async recognizeReceipt() {
if (this.recognizing) return
const sourceItem = this.currentItem
if (!sourceItem.image) {
uni.showToast({
title: '无小票图片可识别',
icon: 'none'
})
return
}
try {
this.recognizing = true
uni.showLoading({
title: '识别中...'
})
const res = await aiReq({
imgUrl: sourceItem.image
})
const content = res && res.data && res.data.output && res.data.output.choices && res.data.output.choices[0]
&& res.data.output.choices[0].message ? res.data.output.choices[0].message.content : null
const rawText = Array.isArray(content)
? (content[0] && typeof content[0] === 'object' ? (content[0].text || content[0].content || '') : content[0])
: content
const parsed = this.parseAiJson(rawText)
if (!parsed) {
uni.hideLoading()
uni.showToast({
title: '识别失败',
icon: 'none'
})
return
}
const orderNo = this.normalizeAiField(parsed.orderNo || parsed.order_no || parsed.orderId || parsed.order_id || '')
const amount = this.normalizeAiField(parsed.amount || parsed.totalAmount || parsed.total_amount || parsed.total || parsed.money || '')
const date = this.normalizeAiField(parsed.date || parsed.printDate || parsed.print_date || parsed.tradeDate || parsed.trade_date || '')
if (!orderNo && !amount && !date) {
uni.hideLoading()
uni.showToast({
title: '未识别到可填充字段',
icon: 'none'
})
return
}
if (orderNo) this.editForm.orderNo = orderNo
if (amount) this.editForm.amount = amount
if (date) this.editForm.date = date
uni.hideLoading()
uni.showToast({
title: '识别成功',
icon: 'success'
})
} catch (error) {
uni.hideLoading()
uni.showToast({
title: '识别失败',
icon: 'none'
})
} finally {
this.recognizing = false
}
},
getDetailIncome(detail) {
const source = this.resolveDetailRewardSource(detail)
const num = this.parseQuantity(detail && (detail.num || detail.qty || 0))
if (!Number.isFinite(num) || num <= 0) return 0
const income = this.getEffectiveIncome(source)
const extraIncome = this.getEffectiveExtraIncome(source)
return (income + extraIncome) * num
},
resolveDetailRewardSource(detail = {}) {
const product = detail && detail.product ? detail.product : {}
return {
income: this.pickFirstNumber([detail.income, product.income, detail.productIncome, detail.bonus], 0),
extraIncome: this.pickFirstNumber([detail.extraIncome, product.extraIncome, detail.extra_bonus, detail.extraBonus], 0),
extraIncomeList: detail.extraIncomeList || detail.extra_income_list || product.extraIncomeList || product.extra_income_list || [],
startTime: detail.startTime || detail.start_time || product.startTime || product.start_time || '',
endTime: detail.endTime || detail.end_time || product.endTime || product.end_time || '',
extraStartTime: detail.extraStartTime || detail.extra_start_time || product.extraStartTime || product.extra_start_time || '',
extraEndTime: detail.extraEndTime || detail.extra_end_time || product.extraEndTime || product.extra_end_time || ''
}
},
resolveEditIncomeValue(detail = {}) {
if (detail && detail._preferSingleIncomeDisplay) {
const singleRaw = detail.incomeSingle ?? detail.income_single
if (singleRaw !== undefined && singleRaw !== null && String(singleRaw).trim() !== '') {
const single = Number(singleRaw)
if (Number.isFinite(single)) return single
}
}
const source = this.resolveDetailRewardSource(detail)
return this.getEffectiveIncome(source)
},
resolveEditExtraIncomeValue(detail = {}) {
if (detail && detail._preferSingleIncomeDisplay) {
const singleRaw = detail.extraIncomeSingle ?? detail.extra_income_single
if (singleRaw !== undefined && singleRaw !== null && String(singleRaw).trim() !== '') {
const single = Number(singleRaw)
if (Number.isFinite(single)) return single
}
}
const raw = detail && detail.extraIncome
if (raw !== undefined && raw !== null && String(raw).trim() !== '') {
const direct = Number(raw)
return Number.isFinite(direct) ? direct : 0
}
const source = this.resolveDetailRewardSource(detail)
return this.getEffectiveExtraIncome(source)
},
calcTotalIncome(list) {
const items = Array.isArray(list) ? list : []
return items.reduce((sum, detail) => sum + this.getDetailIncome(detail), 0)
},
handleApprove() {
const rawIncome = this.calcTotalIncome(this.currentItem.detailList)
const income = Number(rawIncome.toFixed(2))
if (!income || income <= 0) {
uni.showToast({
title: '请确认后再通过',
icon: 'none'
})
return
}
this.$emit('approve', {item: this.item, income})
},
normalizeDetailResponse(data) {
const detail = data && typeof data === 'object' ? {...data} : {}
const list = Array.isArray(detail.detailList) ? detail.detailList : []
detail.detailList = list
return detail
},
resolveManufacturer(detail) {
if (!detail) return '-'
const product = detail.product || {}
const value = detail.makerAbb
|| detail.manufacturer
|| detail.vendor
|| detail.factory
|| detail.brand
|| product.makerAbb
|| product.manufacturer
|| product.vendor
|| product.factory
|| product.brand
return this.normalizeText(value)
},
resolveCode(detail) {
if (!detail) return '-'
const value = detail.goodsCode
|| detail.code
|| detail.productCode
|| detail.skuCode
|| detail.barcode
return this.normalizeText(value)
},
resolveSpec(detail) {
if (!detail) return '-'
const product = detail.product || {}
const value = detail.spec
|| detail.specification
|| detail.standard
|| product.spec
|| product.specification
|| product.standard
return this.normalizeText(value)
},
resolveAmount(detail) {
if (!detail) return ''
return detail.income
},
resolveExtraAmount(detail) {
if (!detail) return ''
return detail.extraIncome
},
resolveExtraIncomeValue(source = {}) {
const candidates = [
source.extraIncome,
source.extra_income,
source.extraBonus,
source.extra_bonus,
source.otherIncome,
source.other_income
]
for (let i = 0; i < candidates.length; i++) {
const raw = candidates[i]
if (raw === '' || raw === null || raw === undefined) continue
const num = Number(raw)
if (Number.isFinite(num)) return num
}
return 0
},
hasExtraIncome(source = {}) {
return this.resolveExtraIncomeValue(source) !== 0
},
formatExtraIncome(source = {}) {
return this.resolveExtraIncomeValue(source).toFixed(2)
},
normalizeText(value) {
if (value === 0) return '0'
if (value === null || value === undefined || value === '') return '-'
return String(value)
},
formatMoney(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
if (Number.isFinite(num)) {
return `¥${num.toFixed(2)}`
}
return String(value)
}
}
}
</script>
<style lang="scss" scoped>
@import './record.scss';
.share-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 30;
}
.share-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: 88%;
max-height: 70vh;
overflow: auto;
z-index: 40;
}
.popup-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.close-btn {
color: var(--text-sub);
font-size: 24rpx;
}
.record-toggle {
margin-top: 8rpx;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4rpx;
font-size: 24rpx;
color: #2fc6a7;
}
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.label {
color: var(--text-main);
font-size: 26rpx;
min-width: 140rpx;
}
.input {
flex: 1;
text-align: right;
font-size: 26rpx;
color: var(--text-main);
}
.picker-input {
flex: 1;
}
.picker-input-inner {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8rpx;
min-height: 40rpx;
}
.picker-text {
color: var(--text-main);
font-size: 26rpx;
}
.picker-placeholder {
color: #9ca3af;
font-size: 26rpx;
}
.merchant-picker {
padding: 20rpx 24rpx 30rpx;
}
.merchant-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.picker-action {
font-size: 26rpx;
color: #6b7280;
}
.picker-action.primary {
color: #2fc6a7;
font-weight: 700;
}
.picker-title {
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.merchant-picker-search {
margin: 6rpx 0 12rpx;
}
.merchant-search-input {
width: 100%;
background: #f3f4f6;
border-radius: 12rpx;
padding: 12rpx 16rpx;
font-size: 26rpx;
color: #1f2937;
}
.merchant-search-placeholder {
color: #9ca3af;
}
.merchant-picker-loading {
padding: 20rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
.merchant-picker-view {
height: 50vh;
}
.merchant-picker-item {
height: 64px;
padding: 0 12rpx;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 26rpx;
color: #1f2937;
line-height: 1.3;
white-space: normal;
word-break: break-all;
}
.form-value {
flex: 1;
text-align: right;
font-size: 26rpx;
color: var(--text-main);
}
.bonus-text {
color: #ff3b30;
font-weight: 700;
}
.disabled-chip {
color: #9ca3af;
border-color: #e5e7eb;
background: #f3f4f6;
opacity: 0.8;
}
.total-bonus-row {
border-bottom: none;
}
.total-bonus-wrap {
flex: 1;
}
.total-bonus-value {
color: #ff2121;
font-weight: 700;
text-align: right;
}
.total-bonus-breakdown {
margin-top: 4rpx;
font-size: 22rpx;
color: #6b7280;
text-align: right;
}
.detail-title {
margin-top: 12rpx;
font-weight: 700;
color: var(--text-main);
}
.detail-actions {
margin-top: 8rpx;
display: flex;
justify-content: flex-end;
}
.ghost-btn {
color: var(--brand-primary);
border: 1rpx solid var(--brand-primary);
padding: 6rpx 16rpx;
border-radius: 999rpx;
font-size: 24rpx;
}
.edit-detail-list {
margin-top: 8rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.edit-detail-item {
border: 1rpx dashed #dfe8f5;
border-radius: 12rpx;
padding: 12rpx;
}
.edit-detail-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
}
.edit-detail-index {
color: var(--text-sub);
font-size: 24rpx;
}
.edit-detail-remove {
color: #ff3b30;
font-size: 24rpx;
}
.submit {
margin-top: 16rpx;
padding: 14rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.popup-actions {
margin-top: 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.popup-actions .submit {
margin-top: 0;
flex: 1;
}
.ghost-action {
flex: 1;
height: 60rpx;
border-radius: 999rpx;
border: 1rpx solid #d1d5db;
color: #374151;
background: #f9fafb;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
}
.product-picker {
padding: 20rpx 24rpx 30rpx;
}
.product-scroll {
max-height: 60vh;
}
.product-option {
padding: 18rpx 12rpx;
border-bottom: 1rpx solid #edf1f7;
}
.product-option-name {
font-size: 26rpx;
color: #111827;
font-weight: 700;
}
.product-option-meta {
margin-top: 4rpx;
font-size: 24rpx;
color: #4b5563;
}
.copy-search-row {
display: flex;
gap: 10rpx;
align-items: center;
}
.copy-search-input {
flex: 1;
height: 70rpx;
line-height: 70rpx;
border-radius: 12rpx;
background: #f3f4f6;
padding: 0 16rpx;
font-size: 26rpx;
}
.copy-search-btn {
min-width: 110rpx;
height: 70rpx;
border-radius: 12rpx;
background: var(--brand-primary);
color: #ffffff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.copy-loading,
.copy-empty {
margin-top: 14rpx;
font-size: 24rpx;
color: var(--text-sub);
}
.copy-user-list {
margin-top: 14rpx;
max-height: 420rpx;
}
.copy-user-item {
padding: 12rpx;
border-radius: 12rpx;
background: #f9fafb;
margin-bottom: 10rpx;
border: 1rpx solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.copy-user-item-active {
border-color: var(--brand-primary);
background: #ecfdf5;
}
.copy-user-name {
font-size: 26rpx;
color: var(--text-main);
}
.copy-user-phone {
font-size: 22rpx;
color: var(--text-sub);
}
</style>