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

728 lines
22 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 class="my-share-page">
<view class="page-hero">
<AppNavbar title="我的分享" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>我的分享</text>
</view>
<text class="intro-text">查看已提交的晒单记录</text>
</view>
</view>
<view class="card block">
<view class="section-title">筛选查询</view>
<view class="filter-row">
<view class="filter-chip" @tap="openChainPicker">{{ chainLabel }}</view>
<view class="filter-chip" @tap="openDatePicker('start')">{{ filterForm.startDate || '开始日期' }}</view>
<view class="filter-chip" @tap="openDatePicker('end')">{{ filterForm.endDate || '结束日期' }}</view>
</view>
<view class="keyword-row">
<input
v-model="filterForm.merchantKeyword"
class="keyword-input"
type="text"
confirm-type="search"
placeholder="请输入门店关键词"
placeholder-class="keyword-placeholder"
@confirm="handleQuery"
/>
<view class="filter-btn primary" @tap="handleQuery">查询</view>
<view class="filter-btn" @tap="handleReset">重置</view>
</view>
</view>
<view class="block">
<view class="section-title">记录列表</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="shares.length === 0" class="empty">暂无分享记录</view>
<view v-else-if="filteredShares.length === 0" class="empty">暂无匹配记录</view>
<view v-else>
<view class="record" v-for="item in filteredShares" :key="item._rowKey">
<view class="record-meta">门店:{{ item.merchantName || item.merchant_name || '未命名门店' }}</view>
<!-- <view class="record-meta" style="color: red">-->
<!-- 红包:¥{{ formatPrice(item.income || 0) }}-->
<!-- <text v-if="hasExtraIncome(item)">(含额外加提:{{ formatPrice(resolveExtraIncome(item)) }}元)</text>-->
<!-- </view>-->
<view class="record-meta">提交人:{{ item.owner || '-' }} · {{ item.date || item.activityDateText }}</view>
<view class="record-meta">留言:{{ item.remark || '-' }}</view>
<view class="record-toggle" @tap="openDetail(item)">
<text>点击查看详情</text>
<u-icon name="arrow-right" size="20" color="#2fc6a7"/>
</view>
<view v-if="item.statusValue !== 1" class="record-actions">
<view class="flex-1 mx-15">
<view
:class="['chip w-90p danger-chip', deletingId === item._rowKey ? 'disabled-chip' : '']"
@tap="handleVoid(item)"
>{{ deletingId === item._rowKey ? '作废中...' : '作废' }}</view>
</view>
</view>
</view>
</view>
</view>
<u-picker
:show="showChainPicker"
:columns="[chainOptions]"
:loading="chainLoading"
:defaultIndex="chainPickerIndex"
keyName="text"
@confirm="onChainConfirm"
@cancel="closeChainPicker"
@close="closeChainPicker"
/>
<u-datetime-picker
:show="showStartPicker"
:value="startPickerValue"
mode="date"
@confirm="e => onDateConfirm('start', e)"
@cancel="closeDatePicker('start')"
@close="closeDatePicker('start')"
/>
<u-datetime-picker
:show="showEndPicker"
:value="endPickerValue"
mode="date"
@confirm="e => onDateConfirm('end', e)"
@cancel="closeDatePicker('end')"
@close="closeDatePicker('end')"
/>
</view>
</template>
<script>
import dayjs from "dayjs";
import {shareDeleteReq, shareUserListReq} from "@/api/share";
import AppNavbar from "@/components/AppNavbar.vue";
import {merchantGroupByChainReq} from "@/api/shop";
export default {
components: {AppNavbar},
data() {
return {
shares: [],
loading: false,
chainLoading: false,
showChainPicker: false,
showStartPicker: false,
showEndPicker: false,
chainOptions: [{id: '', text: '全部连锁'}],
chainMerchantMap: {},
deletingId: '',
filterForm: {
chainId: '',
merchantKeyword: '',
startDate: '',
endDate: ''
}
}
},
computed: {
chainLabel() {
const selected = this.chainOptions.find(item => item.id === this.filterForm.chainId)
return (selected && selected.text) || '连锁筛选'
},
chainPickerIndex() {
const idx = this.chainOptions.findIndex(item => item.id === this.filterForm.chainId)
return [idx > -1 ? idx : 0]
},
startPickerValue() {
return this.filterForm.startDate ? dayjs(this.filterForm.startDate).valueOf() : Date.now()
},
endPickerValue() {
return this.filterForm.endDate ? dayjs(this.filterForm.endDate).valueOf() : Date.now()
},
filteredShares() {
const list = Array.isArray(this.shares) ? this.shares : []
const keyword = (this.filterForm.merchantKeyword || '').trim().toLowerCase()
const startTs = this.filterForm.startDate
? dayjs(`${this.filterForm.startDate} 00:00:00`).valueOf()
: null
const endTs = this.filterForm.endDate
? dayjs(`${this.filterForm.endDate} 23:59:59`).valueOf()
: null
return list.filter(item => {
if (this.filterForm.chainId && String(item.chainIdNormalized || '') !== String(this.filterForm.chainId)) {
return false
}
if (keyword) {
const merchantText = String(item.merchantName || item.merchant_name || '').toLowerCase()
if (!merchantText.includes(keyword)) return false
}
if ((startTs || endTs) && !Number.isFinite(item.activityTimestamp)) {
return false
}
if (startTs && item.activityTimestamp < startTs) {
return false
}
if (endTs && item.activityTimestamp > endTs) {
return false
}
return true
})
}
},
onShow() {
this.initPage()
},
methods: {
async initPage() {
await this.fetchChainList()
await this.fetchShareList()
},
async fetchChainList() {
this.chainLoading = true
try {
const {data} = await merchantGroupByChainReq({})
const list = this.unwrapList(data)
const chainList = list
.map((item, index) => this.normalizeMerchantChain(item, index))
.filter(item => item.id !== '' && item.id !== null && item.id !== undefined)
const chainOptions = [{id: '', text: '全部连锁'}]
const chainMerchantMap = {}
chainList.forEach(chain => {
chainOptions.push({
id: chain.id,
text: chain.text
})
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
merchants.forEach(merchant => {
const merchantId = merchant.id
if (merchantId === '' || merchantId === null || merchantId === undefined) return
chainMerchantMap[String(merchantId)] = {
chainId: chain.id,
chainName: chain.text
}
})
})
this.chainOptions = chainOptions
this.chainMerchantMap = chainMerchantMap
} catch (error) {
this.chainOptions = [{id: '', text: '全部连锁'}]
this.chainMerchantMap = {}
} finally {
this.chainLoading = false
}
},
buildQueryParams() {
const params = {}
const chainId = this.filterForm.chainId
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (keyword) {
params.merchantName = keyword
params.keyword = keyword
}
if (startDate) {
params.createTimeStart = `${startDate} 00:00:00`
params.dateStart = startDate
}
if (endDate) {
params.createTimeEnd = `${endDate} 23:59:59`
params.dateEnd = endDate
}
return params
},
async fetchShareList() {
this.loading = true
try {
const {data} = await shareUserListReq(this.buildQueryParams())
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
this.shares = list.map((item, index) => this.normalizeShareItem(item, index))
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.log(err)
} finally {
this.loading = false
}
},
handleQuery() {
if (!this.validateDateRange()) return
this.fetchShareList()
},
async handleReset() {
this.filterForm = {
chainId: '',
merchantKeyword: '',
startDate: '',
endDate: ''
}
await this.fetchShareList()
},
openChainPicker() {
this.showChainPicker = true
},
closeChainPicker() {
this.showChainPicker = false
},
onChainConfirm(e) {
const selected = e.value && e.value.length ? e.value[0] : null
this.filterForm.chainId = selected ? (selected.id ?? '') : ''
this.showChainPicker = false
this.handleQuery()
},
openDatePicker(type) {
if (type === 'start') {
this.showStartPicker = true
return
}
this.showEndPicker = true
},
closeDatePicker(type) {
if (type === 'start') {
this.showStartPicker = false
return
}
this.showEndPicker = false
},
onDateConfirm(type, event) {
const raw = event && event.value !== undefined ? event.value : Date.now()
const value = dayjs(raw).format('YYYY-MM-DD')
const prevStartDate = this.filterForm.startDate
const prevEndDate = this.filterForm.endDate
if (type === 'start') {
this.filterForm.startDate = value
this.showStartPicker = false
} else {
this.filterForm.endDate = value
this.showEndPicker = false
}
if (!this.validateDateRange()) {
this.filterForm.startDate = prevStartDate
this.filterForm.endDate = prevEndDate
return
}
this.fetchShareList()
},
validateDateRange() {
const {startDate, endDate} = this.filterForm
if (!startDate || !endDate) return true
const startTs = dayjs(`${startDate} 00:00:00`).valueOf()
const endTs = dayjs(`${endDate} 23:59:59`).valueOf()
if (startTs <= endTs) return true
uni.showToast({
title: '开始日期不能晚于结束日期',
icon: 'none'
})
return false
},
normalizeShareItem(item, index) {
const detailListRaw = item.detailList || item.list || []
const detailList = this.parseJsonMaybe(detailListRaw)
const receiptImages = this.resolveReceiptImages(item)
const chainInfo = this.resolveShareChain(item)
const activityRaw = item.date || item.shareDate || item.createTime || item.createdAt || item.create_time || ''
const activityTimestamp = this.parseDateToTimestamp(activityRaw)
const statusValue = Number(item.status)
let statusLabel = '状态未知'
let statusClass = 'pending'
if (statusValue === 0) statusLabel = '待审核'
if (statusValue === 1) {
statusLabel = '已通过'
statusClass = 'success'
}
if (statusValue === 2) {
statusLabel = '已驳回'
statusClass = 'danger'
}
return {
...item,
_rowKey: this.buildRowKey(item, index),
detailList: Array.isArray(detailList) ? detailList : [],
receiptImages,
chainIdNormalized: chainInfo.chainId,
chainName: chainInfo.chainName,
activityDateText: activityRaw || '-',
activityTimestamp,
statusValue,
statusLabel,
owner: item.user.realName || item.user.nickname || '-',
statusClass
}
},
buildRowKey(item, index) {
const keySeed = item.id ?? item.shareId ?? item.orderNo ?? item.createTime ?? item.date
return keySeed !== undefined && keySeed !== null && String(keySeed).trim() !== ''
? `share_${keySeed}`
: `share_${index}`
},
buildDetailCacheKey(item = {}) {
const seed = item._rowKey || item.id || item.shareId || Date.now()
return `share_detail_${seed}`
},
openDetail(item) {
if (!item) return
const cacheKey = this.buildDetailCacheKey(item)
try {
uni.setStorageSync(cacheKey, item)
} catch (error) {
}
uni.navigateTo({
url: `/pages/share/detail?cacheKey=${encodeURIComponent(cacheKey)}`
})
},
async handleVoid(item) {
if (!item || Number(item.statusValue) === 1) return
if (this.deletingId) return
const targetId = item.id ?? item.shareId ?? ''
if (targetId === '' || targetId === null || targetId === undefined) {
uni.showToast({
title: '记录ID缺失,无法作废',
icon: 'none'
})
return
}
const confirmRes = await new Promise(resolve => {
uni.showModal({
title: '确认作废',
content: '作废后不可恢复,是否继续?',
confirmText: '确定',
cancelText: '取消',
success: resolve,
fail: () => resolve({confirm: false})
})
})
if (!confirmRes || !confirmRes.confirm) return
try {
this.deletingId = item._rowKey
const res = await shareDeleteReq(targetId)
if (res && res.code !== undefined && Number(res.code) !== 0) {
throw new Error('delete failed')
}
uni.showToast({
title: '作废成功',
icon: 'success'
})
await this.fetchShareList()
} catch (error) {
uni.showToast({
title: '作废失败',
icon: 'none'
})
} finally {
this.deletingId = ''
}
},
resolveShareChain(item) {
const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? ''
const merchantId = item.merchantId ?? item.merchant_id ?? ''
const directName = item.chainName || item.chain_name || item.chain || item.chainAbb || ''
let chainId = rawChainId
let chainName = directName
const merchantChain = merchantId !== '' && merchantId !== null && merchantId !== undefined
? this.chainMerchantMap[String(merchantId)]
: null
if (!chainId && merchantChain) {
chainId = merchantChain.chainId
}
if (!chainName && merchantChain) {
chainName = merchantChain.chainName
}
if (!chainName && chainId !== '' && chainId !== null && chainId !== undefined) {
const selected = this.chainOptions.find(option => String(option.id) === String(chainId))
chainName = selected ? selected.text : ''
}
return {
chainId: chainId ?? '',
chainName: chainName || '-'
}
},
parseJsonMaybe(value) {
if (value === null || value === undefined || value === '') return []
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch (err) {
return []
}
}
return value
},
resolveReceiptImages(item = {}) {
const candidates = [
item.image,
item.images,
item.receiptPhoto,
item.receiptPhotos,
item.receipt_photo,
item.receipt_photos
]
const urls = []
candidates.forEach(value => {
this.appendReceiptValue(urls, value)
})
const unique = Array.from(new Set(urls.filter(url => /^https?:\/\//.test(url) || /^\/\//.test(url))))
return unique
},
appendReceiptValue(target, value) {
if (value === null || value === undefined || value === '') return
if (Array.isArray(value)) {
value.forEach(item => this.appendReceiptValue(target, item))
return
}
if (typeof value === 'object') {
const url = value.url || value.path || value.src || value.image || value.img
if (url) this.appendReceiptValue(target, url)
return
}
const text = String(value).trim()
if (!text) return
if (text.startsWith('[') || text.startsWith('{')) {
try {
const parsed = JSON.parse(text)
this.appendReceiptValue(target, parsed)
return
} catch (err) {
}
}
if (text.includes(',')) {
text.split(',').forEach(part => this.appendReceiptValue(target, part))
return
}
target.push(text)
},
parseDateToTimestamp(value) {
if (value === null || value === undefined || value === '') return NaN
const raw = String(value).trim()
if (!raw) return NaN
if (/^-?\d+(\.\d+)?$/.test(raw)) {
const num = Number(raw)
if (!Number.isFinite(num)) return NaN
const millis = Math.abs(num) < 1e12 ? num * 1000 : num
return millis
}
const parsed = dayjs(raw)
if (!parsed.isValid()) return NaN
return parsed.valueOf()
},
normalizeMerchantChain(item, index) {
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? ''
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
? `chain_${index}`
: rawChainId
const childCandidates = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
let rawChildren = []
for (let i = 0; i < childCandidates.length; i++) {
const list = item[childCandidates[i]]
if (Array.isArray(list) && list.length) {
rawChildren = list
break
}
}
const merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
return {
id: chainId,
text: chainName,
merchants
}
},
normalizeMerchant(item) {
const id = item.id ?? item.merchantId ?? item.merchant_id
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
return {
id,
text
}
},
pickFirstText(source, keys = []) {
if (!source || typeof source !== 'object') return ''
for (let i = 0; i < keys.length; i++) {
const value = source[keys[i]]
if (value !== undefined && value !== null && String(value).trim()) {
return String(value).trim()
}
}
return ''
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
for (let i = 0; i < keys.length; i++) {
const value = data[keys[i]]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
const nested = this.unwrapList(value)
if (nested.length) return nested
}
}
return [data]
},
formatPrice(value) {
const num = Number(value || 0)
return num.toFixed(2)
},
resolveExtraIncome(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.resolveExtraIncome(source) !== 0
}
}
}
</script>
<style lang="scss" scoped>
@import '../audit/components/record.scss';
.my-share-page {
min-height: 100vh;
//padding: 0 20rpx 40rpx;
background: #f5f5f5;
}
.page-hero {
//padding: 30rpx 6rpx 18rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat
top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.block {
margin: 18rpx 20rpx 0;
}
.filter-row {
margin-top: 14rpx;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.filter-chip {
padding: 8rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
color: #2fc6a7;
font-size: 22rpx;
background: #f3fffb;
}
.keyword-row {
margin-top: 14rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.keyword-input {
flex: 1;
height: 64rpx;
border-radius: 14rpx;
border: 1rpx solid #d1d5db;
background: #ffffff;
padding: 0 16rpx;
font-size: 24rpx;
color: #374151;
}
.keyword-placeholder {
color: #9ca3af;
}
.filter-btn {
min-width: 96rpx;
height: 64rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid #d1d5db;
color: #6b7280;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
.filter-btn.primary {
border-color: #2fc6a7;
background: #2fc6a7;
color: #ffffff;
}
.record-toggle {
margin-top: 8rpx;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4rpx;
font-size: 24rpx;
color: #2fc6a7;
}
.disabled-chip {
color: #9ca3af;
border-color: #e5e7eb;
background: #f3f4f6;
opacity: 0.8;
}
.empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
</style>