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

1640 lines
44 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="share-page">
<page-meta :page-style="pageStyle"/>
<view class="share-hero">
<AppNavbar title="药分享">
</AppNavbar>
<view class="stats-card">
<view class="flex justify-between items-center w-100p">
<!-- <view class="stat-item" @click="jumpWithAuth('/pages/share/mine')">-->
<!-- <view class="stat-value">-->
<!-- <text class="stat-number">{{-->
<!-- data.income-->
<!-- }}-->
<!-- </text>-->
<!-- <text class="stat-unit">元</text>-->
<!-- </view>-->
<!-- <text class="stat-label">我的晒单收益</text>-->
<!-- </view>-->
<view class="stat-item" @click="jumpWithAuth('/pages/share/mine')">
<view class="stat-value">
<text class="stat-number">{{ data.num }}</text>
<text class="stat-unit">个</text>
</view>
<text class="stat-label">我的晒单数量</text>
</view>
<!-- <view class="stat-action" @click="jumpWithAuth('/pages/bonus/withdraw')">去提现</view>-->
</view>
<view class="flex justify-center items-center w-100p pt-20 border-top" v-if="isLoggedIn && user.canAuth === 1">
<!-- <view class="stat-item flex-1 justify-center items-center">-->
<!-- <view class="stat-value">-->
<!-- <text class="stat-number text-main">{{-->
<!-- data.totalIncome-->
<!-- }}-->
<!-- </text>-->
<!-- <text class="stat-unit">元</text>-->
<!-- </view>-->
<!-- <text class="stat-label">合计晒单收益</text>-->
<!-- </view>-->
<view class="stat-item flex-1 justify-center items-center">
<view class="stat-value">
<text class="stat-number text-main">{{ data.totalNum }}</text>
<text class="stat-unit">个</text>
</view>
<text class="stat-label">合计晒单数量</text>
</view>
</view>
</view>
</view>
<!-- 今日晒单 -->
<view class="today-head">
<view class="city" @tap="openAreaPicker">
<u-icon name="map" size="26" color="#27c7a4"/>
<text>{{ currentCity }}</text>
</view>
<view class="head-center">
<image class="head-icon"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/a215c967f5d89626826da5bc3bec9130.png"
mode="heightFix"/>
</view>
<view v-if="canShowTodayMore" class="today-more" @tap="showAllTodayEntries">更多</view>
</view>
<view class="filter-chips">
<view class="filter-chip" @tap="openMerchantPicker">{{ merchantLabel }}</view>
</view>
<view class="today-list">
<template v-if="displayedTodayEntries.length">
<view class="today-card" v-for="(item, index) in displayedTodayEntries" :key="index" @click="showDetail(item)">
<view class="today-row">
<text class="today-label">匹配门店:</text>
<text class="today-value">{{ item.merchantName }}</text>
</view>
<view class="today-row">
<text class="today-label">订单号:</text>
<text class="today-value">{{ item.orderNo }}</text>
</view>
<view class="today-row today-row-space">
<text class="today-time">分享时间:{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</text>
<view class="today-distance">
<u-icon name="map" size="24" color="#ff9f43"/>
<text>距离{{ item.distance || '100米' }}</text>
</view>
</view>
</view>
</template>
<u-empty v-else
icon="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/ddcc1bbd561d97128ed19407b07e299b.png"
text="暂无数据"/>
</view>
<!-- 分享产品清单 -->
<view class="product-section">
<view class="section-head">
<view class="section-left" style="width: calc(100% - 100rpx)">
<image class="head-icon"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/17b2063b8acca65f86370593849e607d.png"
mode="heightFix"/>
</view>
<view
v-if="canShowProductMore"
class="section-more product-more"
@tap="showAllProductEntries"
>更多>>
</view>
</view>
<view class="product-search">
<view class="search-box">
<u-icon name="search" size="28" color="#9ca3af"/>
<input class="search-input" placeholder="请输入关键字搜索" v-model="productKeyword"/>
</view>
<view class="search-btn" @click="searchProduct">搜索</view>
</view>
<view class="product-grid">
<template v-if="displayedShareProducts.length">
<view class="product-card" v-for="(item, index) in displayedShareProducts" :key="index">
<view class="flex justify-center items-center">
<u-image
:src="item.product && item.product.cover ? item.product.cover : 'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/35fa0fb100376d9925f872fdb752b5f1.jpeg'"
width="180rpx"
height="180rpx"
radius="12rpx"
mode="aspectFill"
@click="preview(item.product && item.product.cover ? item.product.cover : 'https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/35fa0fb100376d9925f872fdb752b5f1.jpeg')"
/>
</view>
<view class="product-info">
<view class="product-name text-center">{{ item.goodsName }}</view>
<u-line dashed/>
<view class="product-meta text-center">{{ item.product ? item.product.spec : item.spec }}</view>
<u-line dashed/>
<view class="product-meta text-center">{{ item.product ? item.product.makerAbb : '' }}</view>
<u-line dashed/>
</view>
<!-- <view class="product-bonus">-->
<!-- <text>赚¥{{ item.income || 0 }}</text>-->
<!-- <image class="bonus-tag"-->
<!-- src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/2dedec3908af49f55012691f7e22b408.png"-->
<!-- mode="aspectFit"/>-->
<!-- </view>-->
<u-line dashed/>
<view class="product-bonus">
<text>共分享{{ item.totalNum ? item.totalNum : 0 }}次</text>
</view>
</view>
</template>
</view>
</view>
<view class="rank-section">
<view class="section-head rank-head">
<view class="section-left" style="width: calc(100% - 100rpx)">
<image class="head-icon"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/fe741abe2e0dd7e39a60cb4deba45c4e.png"
mode="heightFix"/>
<!-- <text class="section-title">门店晒单排名</text>-->
</view>
</view>
<view class="rank-list">
<template v-if="brandRanks.length">
<view class="rank-item" v-for="(item, idx) in brandRanks" :key="idx">
<view class="rank-left">
<view class="rank-badge">
<image v-if="idx === 0" class="rank-medal"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/99b7a70575473d52e38c765a8635428c.png"
mode="aspectFit"/>
<image v-else-if="idx === 1" class="rank-medal"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/3a984885c03dbd31f1bd998f98c41493.png"
mode="aspectFit"/>
<image v-else-if="idx === 2" class="rank-medal"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/722383e4ed2ba2e98cd04ce3ee5c4b8f.png"
mode="aspectFit"/>
<text v-else class="rank-num">{{ idx + 1 }}</text>
</view>
<text class="rank-name">{{ item.merchantName || item.merchant_name || '门店' }}</text>
</view>
<view class="rank-right">
<!-- <text class="rank-reward">奖励¥{{ item.amount || 0 }}</text>-->
<text class="rank-count">晒单 {{ item.shareList.length || 0 }}</text>
</view>
</view>
</template>
<u-empty v-else
icon="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/ddcc1bbd561d97128ed19407b07e299b.png"
text="暂无数据"/>
</view>
<!-- <view class="rank-rule" @click="openRulePopup">红包奖励规则</view>-->
</view>
<view class="footer">
<view class="share-action" @click="handleShare">我要分享</view>
</view>
<view v-if="showDetailPopup" class="share-popup-mask" @tap="closeDetailPopup" @touchmove.stop.prevent></view>
<view v-if="showDetailPopup" class="share-popup card" @tap.stop>
<view class="popup-head">
<view class="section-title">晒单详情</view>
<view class="close-btn" @tap="closeDetailPopup">关闭</view>
</view>
<view class="record-meta">门店:{{ detailEntry.merchantName || detailEntry.merchant_name || '-' }}</view>
<!-- <view class="record-meta text-red">-->
<!-- 红包:¥{{ formatPrice(detailEntry.income || 0) }}-->
<!-- <text v-if="hasExtraIncome(detailEntry)">(含额外加提:{{ formatPrice(resolveExtraIncome(detailEntry)) }}元)</text>-->
<!-- </view>-->
<view class="record-meta">
提交人:{{ detailEntry.owner || '-' }} · {{ detailEntry.date || detailEntry.shareDate || detailEntry.activityDateText || '-' }}
</view>
<view class="record-meta">留言:{{ detailEntry.remark || '-' }}</view>
<view class="detail-title">商品明细</view>
<view v-if="detailEntry.detailList.length === 0" class="detail-empty">暂无明细</view>
<view v-else class="detail-list">
<view class="detail-item" v-for="(detail, idx) in detailEntry.detailList" :key="idx">
<view class="detail-head">
<view class="detail-name">{{ detail.goodsName || detail.goods_name || 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 || detail.qty || 0 }}</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 text-red">{{ formatMoney(resolveAmount(detail)) }}</text>
</view>
<!-- <view class="detail-kv">-->
<!-- <text class="detail-label">额外加提</text>-->
<!-- <text class="detail-value text-red">{{ formatMoney(resolveExtraAmount(detail)) }}</text>-->
<!-- </view>-->
</view>
</view>
</view>
</view>
<view v-if="showRulePopup" class="share-popup-mask" @tap="closeRulePopup" @touchmove.stop.prevent></view>
<view v-if="showRulePopup" class="share-popup card rule-popup" @tap.stop>
<view class="popup-head">
<view class="section-title">规则</view>
<view class="close-btn" @tap="closeRulePopup">关闭</view>
</view>
<view class="rule-content">{{ rewardRules || '暂无规则' }}</view>
</view>
<AreaPicker
:show="showAreaPicker"
:default-province="currentProvince"
:default-city="currentCity"
@close="showAreaPicker = false"
@confirm="onAreaConfirm"
/>
<u-popup :show="showMerchantPicker" mode="bottom" round="24rpx" @close="closeMerchantPicker">
<view class="merchant-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeMerchantPicker">取消</view>
<text class="picker-title">选择连锁和门店</text>
<view class="picker-action primary" @tap="confirmMerchantPicker">确定</view>
</view>
<view class="merchant-picker-search">
<input
v-model="merchantSearchKeyword"
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="merchantPickerValue"
:indicator-style="merchantIndicatorStyle"
@change="onMerchantPickerChange"
>
<picker-view-column>
<view v-for="(item, idx) in merchantChainOptions" :key="idx" class="merchant-picker-item">{{
item.text
}}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="(item, idx) in merchantStoreOptions" :key="idx" class="merchant-picker-item">{{
item.text
}}
</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
<Login ref="Login" @done="getUserData"/>
</view>
</template>
<script>
import {calcDistanceMeters, formatDistance, getLocationAction} from "@/util";
import {dictDataInfoReq} from "@/api/common";
import {
shareDataReq,
shareDetailPageReq,
shareListReq,
shareUserDataReq
} from "@/api/share";
import dayjs from 'dayjs'
import Login from "@/components/Login.vue";
import AppNavbar from "@/components/AppNavbar.vue";
import AreaPicker from "@/components/AreaPicker.vue";
import {userInfoReq} from "@/api/user";
import {getUserInfo} from "@/util/user";
import {merchantGroupByChainReq} from "@/api/shop";
export default {
components: {Login, AppNavbar, AreaPicker},
data() {
return {
todayEntries: [],
brandRanks: [],
shareProducts: [],
rewardRules: '',
showDetailPopup: false,
showRulePopup: false,
detailEntry: {
detailList: []
},
isLoggedIn: false,
data: {
income: 0,
num: 0,
totalNum: 0,
totalIncome: 0,
},
lat: null,
lng: null,
currentCity: '定位中',
currentProvince: '',
showAreaPicker: false,
showMerchantPicker: false,
merchantLoading: false,
merchantChainList: [],
merchantSearchKeyword: '',
merchantPickerValue: [0, 0],
merchantChainId: '',
merchantId: '',
merchantText: '门店筛选',
showAllToday: false,
showAllProducts: false,
productKeyword: '',
productKeywordApplied: '',
user: {
canAuth: 0
}
}
},
computed: {
isPopupOpen() {
return this.showDetailPopup || this.showRulePopup
},
pageStyle() {
return this.isPopupOpen ? 'overflow: hidden;' : ''
},
displayedTodayEntries() {
const list = Array.isArray(this.todayEntries) ? this.todayEntries : []
return this.showAllToday ? list : list.slice(0, 5)
},
canShowTodayMore() {
return !this.showAllToday && this.todayEntries.length > 5
},
merchantLabel() {
return this.merchantText || '门店筛选'
},
merchantChainOptions() {
const list = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
const keyword = (this.merchantSearchKeyword || '').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: []}]
},
merchantStoreOptions() {
const chainIndex = Number(this.merchantPickerValue[0] || 0)
const currentChain = this.merchantChainOptions[chainIndex] || this.merchantChainOptions[0] || {}
const merchants = Array.isArray(currentChain.merchants) ? currentChain.merchants : []
return merchants.length ? merchants : [{id: '', text: '无门店'}]
},
merchantPickerIndexes() {
const chainList = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
if (!chainList.length) return [0, 0]
let chainIndex = chainList.findIndex(item => item.id === this.merchantChainId)
if (chainIndex < 0 && this.merchantId) {
chainIndex = chainList.findIndex(item => {
const merchants = Array.isArray(item.merchants) ? item.merchants : []
return merchants.some(merchant => merchant.id === this.merchantId)
})
}
if (chainIndex < 0) chainIndex = 0
const merchants = Array.isArray(chainList[chainIndex].merchants) ? chainList[chainIndex].merchants : []
let merchantIndex = merchants.findIndex(item => item.id === this.merchantId)
if (merchantIndex < 0) merchantIndex = 0
return [chainIndex, merchantIndex]
},
merchantIndicatorStyle() {
return 'height: 44px;'
},
displayedShareProducts() {
const list = this.filteredShareProducts
return this.showAllProducts ? list.slice(0, 48) : list.slice(0, 12)
},
canShowProductMore() {
return !this.showAllProducts && this.filteredShareProducts.length > 12
},
filteredShareProducts() {
const list = Array.isArray(this.shareProducts) ? this.shareProducts : []
const keyword = (this.productKeywordApplied || '').trim()
if (!keyword) return list
const normalized = keyword.toLowerCase()
return list.filter(item => {
const name = (item.goodsName || item.productName || '').toString().toLowerCase()
const spec = (item.product ? item.product.spec : item.spec || '').toString().toLowerCase()
const maker = (item.product ? item.product.makerAbb : '').toString().toLowerCase()
return name.includes(normalized) || spec.includes(normalized) || maker.includes(normalized)
})
}
},
watch: {
merchantSearchKeyword() {
this.merchantPickerValue = [0, 0]
}
},
async onShow() {
await this.refreshPageData()
},
async onPullDownRefresh() {
try {
await this.refreshPageData()
} finally {
uni.stopPullDownRefresh()
}
},
methods: {
dayjs,
async refreshPageData() {
if (getUserInfo().token) {
await this.getUserData()
}
const res = await getLocationAction().catch(() => {
uni.showToast({
title: '请允许位置授权',
icon: 'none'
})
})
if (res) {
this.lat = res.latitude
this.lng = res.longitude
this.currentCity = res.city_str
this.currentProvince = res.province_str || ''
}
await this.getShareUserData()
await this.getShareData()
await this.getTodayShare()
await this.getProductList()
await this.getRule()
},
openAreaPicker() {
this.showAreaPicker = true
},
onAreaConfirm({provinceName, cityName}) {
this.currentProvince = provinceName
this.currentCity = cityName
this.showAreaPicker = false
this.getShareData()
this.getTodayShare()
this.getProductList()
},
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(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.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
}
},
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]
},
async getMerchantList() {
this.merchantLoading = true
try {
const {data} = await merchantGroupByChainReq({})
const list = this.unwrapList(data)
const chainList = list
.map((item, index) => this.normalizeMerchantChain(item, index))
.filter(item => Array.isArray(item.merchants) && item.merchants.length)
this.merchantChainList = [{
id: '',
text: '全部连锁',
merchants: [{id: '', text: '全部门店'}]
}].concat(chainList)
} catch (error) {
uni.showToast({
title: '门店列表获取失败',
icon: 'none'
})
} finally {
this.merchantLoading = false
}
},
async openMerchantPicker() {
if (!this.merchantChainList || this.merchantChainList.length <= 1) {
await this.getMerchantList()
}
this.merchantSearchKeyword = ''
this.merchantPickerValue = [...this.merchantPickerIndexes]
this.showMerchantPicker = true
},
closeMerchantPicker() {
this.showMerchantPicker = false
},
onMerchantPickerChange(e) {
const value = e.detail && e.detail.value !== undefined ? e.detail.value : e.detail
const currentChainIndex = Number(this.merchantPickerValue[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.merchantChainOptions[chainIndex] || this.merchantChainOptions[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.merchantPickerValue = [chainIndex, merchantIndex]
},
confirmMerchantPicker() {
const chainIndex = Number(this.merchantPickerValue[0] || 0)
const merchantIndex = Number(this.merchantPickerValue[1] || 0)
const chain = this.merchantChainOptions[chainIndex] || null
const merchants = chain && Array.isArray(chain.merchants) ? chain.merchants : []
const selected = merchants[merchantIndex] || null
if (!selected) return
this.merchantChainId = chain ? (chain.id ?? '') : ''
this.merchantId = selected.id ?? ''
if (!this.merchantId) {
this.merchantText = '门店筛选'
} else {
this.merchantText = `${(chain && chain.text) || '连锁'}/${selected.text || '门店'}`
}
this.showMerchantPicker = false
this.getShareData()
this.getTodayShare()
this.getProductList()
},
async getRule() {
const {data} = await dictDataInfoReq(576)
this.rewardRules = data.dictDataCode
},
async getShareData() {
const params = {
province: this.currentProvince,
city: this.currentCity
}
if (this.merchantId) {
params.merchantId = this.merchantId
}
const {data} = await shareDataReq(params)
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
this.brandRanks = list.sort((a, b) => parseFloat(b.amount) - parseFloat(a.amount))
},
async getShareUserData() {
const {data} = await shareUserDataReq()
this.data = data
},
async getUserData() {
const {data} = await userInfoReq()
this.isLoggedIn = true
this.user = data
},
async getTodayShare() {
const params = {
createTimeStart: dayjs().format('YYYY-MM-DD'),
createTimeEnd: dayjs().format('YYYY-MM-DD 23:59:59'),
province: this.currentProvince,
city: this.currentCity
}
if (this.merchantId) {
params.merchantId = this.merchantId
}
const {data} = await shareListReq(params)
this.todayEntries = data
this.showAllToday = false
this.updateTodayDistances()
},
async getProductList() {
const params = {
page: 1,
limit: 48,
province: this.currentProvince,
city: this.currentCity,
groupByProduct: true
}
if (this.merchantId) {
params.merchantId = this.merchantId
}
const {data} = await shareDetailPageReq(params)
this.shareProducts = data.list
this.shareProducts = this.shareProducts.filter(item => !!item.product).filter((item, index) => index < 48)
this.showAllProducts = false
this.productKeyword = ''
this.productKeywordApplied = ''
},
showDetail(item) {
this.detailEntry = this.normalizeDetailEntry(item || {})
this.showDetailPopup = true
},
closeDetailPopup() {
this.showDetailPopup = false
this.detailEntry = {detailList: []}
},
openRulePopup() {
this.showRulePopup = true
},
closeRulePopup() {
this.showRulePopup = false
},
showAllTodayEntries() {
this.showAllToday = true
},
showAllProductEntries() {
this.showAllProducts = true
},
searchProduct() {
this.productKeywordApplied = (this.productKeyword || '').trim()
this.showAllProducts = false
},
updateTodayDistances() {
if (!Number.isFinite(this.lat) || !Number.isFinite(this.lng)) return
const list = Array.isArray(this.todayEntries) ? this.todayEntries : []
this.todayEntries = list.map(item => {
const merchant = item.merchant || {}
const lat = Number(merchant.lat || merchant.latitude || item.merchantLat || item.merchantLatitude || item.lat || item.latitude)
const lng = Number(merchant.lng || merchant.longitude || item.merchantLng || item.merchantLongitude || item.lng || item.longitude)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return item
const meters = calcDistanceMeters(this.lat, this.lng, lat, lng)
return Object.assign({}, item, {
distance: formatDistance(meters)
})
})
},
handleShare() {
if (!this.isLoggedIn) {
this.$refs.Login.open()
return
}
const lat = Number.isFinite(this.lat) ? this.lat : ''
const lng = Number.isFinite(this.lng) ? this.lng : ''
const province = encodeURIComponent(this.currentProvince || '')
const city = encodeURIComponent(this.currentCity || '')
uni.navigateTo({
url: `/pages/share/create?lat=${lat}&lng=${lng}&province=${province}&city=${city}`
})
},
normalizeDetailEntry(item) {
const rawList = item.detailList || item.list || item.detail_list || []
const detailList = this.parseJsonMaybe(rawList)
return {
...item,
owner: item.owner || (item.user && (item.user.realName || item.user.nickname)) || '-',
activityDateText: item.date || item.shareDate || item.createTime || item.createdAt || item.create_time || '-',
detailList: Array.isArray(detailList) ? detailList : []
}
},
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
},
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 ''
if (detail.income !== undefined && detail.income !== null && detail.income !== '') return detail.income
if (detail.product && detail.product.income !== undefined && detail.product.income !== null && detail.product.income !== '') {
return detail.product.income
}
return ''
},
resolveExtraAmount(detail) {
if (!detail) return ''
if (detail.extraIncome !== undefined && detail.extraIncome !== null && detail.extraIncome !== '') return detail.extraIncome
if (detail.product && detail.product.extraIncome !== undefined && detail.product.extraIncome !== null && detail.product.extraIncome !== '') {
return detail.product.extraIncome
}
return ''
},
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)
},
parseJsonMaybe(value) {
if (value === null || value === undefined || value === '') return null
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch (err) {
return value
}
}
return value
},
preview(cover) {
uni.previewImage({
urls: [cover]
})
},
jumpWithAuth(url) {
if (!this.isLoggedIn) {
this.$refs.Login.open()
return
}
uni.navigateTo({
url
})
}
}
}
</script>
<style lang="scss" scoped>
.share-page {
min-height: 100vh;
//padding: 0 20rpx 50rpx;
background: #e9fbf6 url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/7b74d64d385c371f718cad4a275a5082.png') no-repeat top center / 100% auto;
}
.share-hero {
padding: 0 6rpx 0;
}
.hero-icon {
width: 34rpx;
height: 34rpx;
border-radius: 10rpx;
background: rgba(39, 199, 164, 0.18);
border: 1rpx solid rgba(39, 199, 164, 0.35);
}
.stats-card {
margin: 20rpx;
padding: 18rpx 20rpx;
background: #ffffff;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.08);
border: 1px solid #2fc6a7;
border-top: 20px solid #2fc6a7;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.stat-value {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.stat-number {
font-size: 32rpx;
font-weight: 800;
color: #ff3b30;
}
.stat-unit {
font-size: 22rpx;
color: #6b7280;
}
.stat-label {
font-size: 22rpx;
color: #6b7280;
}
.stat-action {
padding: 10rpx 18rpx;
background: #2fc6a7;
color: #ffffff;
font-size: 24rpx;
font-weight: 700;
border-radius: 999rpx;
}
.today-head {
margin-top: 18rpx;
padding: 0 6rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.filter-chips {
margin: 8rpx 20rpx 0;
display: flex;
justify-content: flex-end;
}
.filter-chip {
padding: 6rpx 14rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
color: #2fc6a7;
font-size: 22rpx;
}
.city {
display: flex;
align-items: center;
gap: 6rpx;
color: #2fc6a7;
font-weight: 700;
font-size: 24rpx;
position: absolute;
top: 10rpx;
left: 20rpx;
}
.today-more {
position: absolute;
top: 10rpx;
right: 20rpx;
font-size: 22rpx;
color: #2fc6a7;
font-weight: 700;
}
.head-center {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #2fc6a7;
flex: 1;
}
.section-title {
font-size: 26rpx;
font-weight: 700;
color: #1f2937;
}
.head-center .section-title,
.section-left .section-title {
color: #2fc6a7;
}
.section-more {
font-size: 22rpx;
color: #2fc6a7;
}
.product-more {
color: #ffffff;
}
.head-icon {
//width: 30rpx;
height: 60rpx;
}
.today-list {
margin-top: 12rpx;
display: flex;
flex-direction: column;
gap: 14rpx;
}
.today-card {
background: #ffffff;
border-radius: 18rpx;
padding: 14rpx 16rpx;
border: 1rpx solid #a7f3d0;
box-shadow: 0 8rpx 16rpx rgba(39, 199, 164, 0.12);
margin: 0 20rpx 0;
}
.today-row {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 24rpx;
color: #374151;
margin-top: 6rpx;
}
.today-row:first-child {
margin-top: 0;
}
.today-row-space {
justify-content: space-between;
}
.today-label {
color: #6b7280;
}
.today-time {
color: #6b7280;
font-size: 22rpx;
}
.today-distance {
display: flex;
align-items: center;
gap: 4rpx;
font-size: 22rpx;
color: #ff8a00;
}
.product-section {
margin-top: 20rpx;
padding: 22rpx 18rpx 26rpx;
border-radius: 24rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/09dd8e6519b44d71410fc82a7e255540.jpg') no-repeat top / cover;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.section-left {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.product-search {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
}
.search-box {
flex: 1;
display: flex;
align-items: center;
gap: 8rpx;
background: #ffffff;
border-radius: 999rpx;
padding: 10rpx 16rpx;
box-shadow: 0 8rpx 14rpx rgba(0, 0, 0, 0.06);
}
.search-input {
flex: 1;
font-size: 24rpx;
color: #1f2937;
}
.search-btn {
padding: 10rpx 16rpx;
background: #2fc6a7;
color: #ffffff;
font-size: 24rpx;
font-weight: 700;
border-radius: 999rpx;
}
.product-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.product-card {
background: #ffffff;
border-radius: 16rpx;
padding: 12rpx;
box-shadow: 0 8rpx 14rpx rgba(0, 0, 0, 0.06);
}
.product-card .product-name {
font-size: 24rpx;
font-weight: 700;
color: #1f2937;
}
.product-card .product-meta {
font-size: 20rpx;
color: #6b7280;
margin-top: 4rpx;
}
.product-info {
margin-top: 10rpx;
}
.product-bonus {
margin-top: 6rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 6rpx;
font-size: 22rpx;
font-weight: 700;
color: #ff6b00;
}
.bonus-tag {
width: 24rpx;
height: 24rpx;
}
.rank-section {
margin: 20rpx;
background: #ffffff;
border-radius: 24rpx;
padding: 22rpx 18rpx 80rpx;
position: relative;
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.06);
}
.rank-head {
margin-bottom: 8rpx;
}
.rank-list {
display: flex;
flex-direction: column;
}
.rank-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14rpx 0;
border-bottom: 1rpx dashed #dbe5f0;
}
.rank-item:last-child {
border-bottom: none;
}
.rank-left {
display: flex;
align-items: center;
gap: 10rpx;
flex: 1;
}
.rank-badge {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.rank-medal {
width: 36rpx;
height: 36rpx;
}
.rank-num {
color: #2fc6a7;
font-weight: 700;
font-size: 24rpx;
}
.rank-name {
color: #1f2937;
font-size: 24rpx;
}
.rank-right {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 22rpx;
color: #ef4444;
}
.rank-reward {
color: #ef4444;
font-weight: 700;
}
.rank-count {
color: #ef4444;
}
.rank-rule {
position: absolute;
right: 18rpx;
bottom: 18rpx;
padding: 10rpx 22rpx;
background: linear-gradient(135deg, #ffb020, #ff7a00);
color: #ffffff;
font-size: 24rpx;
font-weight: 700;
border-radius: 999rpx;
}
.share-action {
margin: 20rpx;
padding: 20rpx 0;
background: linear-gradient(135deg, #33d0b3, #22c3a1);
color: #ffffff;
text-align: center;
font-size: 30rpx;
font-weight: 700;
border-radius: 999rpx;
box-shadow: 0 10rpx 18rpx rgba(34, 195, 161, 0.3);
}
.product {
padding: 16rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.product:last-child {
border-bottom: none;
}
.product-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-name {
font-size: 30rpx;
color: var(--text-main);
}
.product-meta {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 6rpx;
}
.product-amount {
color: #FE6869;
font-size: 24rpx;
margin-top: 6rpx;
}
.product-actions {
display: flex;
justify-content: flex-end;
gap: 12rpx;
margin-top: 12rpx;
}
.rule {
color: var(--text-sub);
margin-bottom: 8rpx;
line-height: 1.5;
font-size: 24rpx;
}
.review {
padding: 12rpx 0;
border-bottom: 1rpx dashed #e3eaf4;
}
.review:last-child {
border-bottom: none;
}
.review-name {
color: var(--text-main);
}
.review-meta {
color: var(--text-sub);
font-size: 24rpx;
margin: 4rpx 0;
}
.review-actions {
display: flex;
gap: 10rpx;
flex-wrap: wrap;
}
.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;
}
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.form-row:last-of-type {
border-bottom: none;
}
.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: 360rpx;
}
.merchant-picker-item {
height: 44px;
line-height: 44px;
text-align: center;
font-size: 26rpx;
color: #1f2937;
}
.form-value {
flex: 1;
text-align: right;
font-size: 26rpx;
color: var(--text-main);
}
.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;
}
.detail-empty {
color: var(--text-sub);
text-align: center;
padding: 12rpx 0;
}
.detail-list {
margin-top: 8rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.detail-item {
border: 1rpx dashed #dfe8f5;
border-radius: 12rpx;
padding: 12rpx;
}
.detail-name {
color: var(--text-main);
font-weight: 600;
flex: 1;
word-break: break-all;
}
.detail-grid {
display: flex;
flex-wrap: wrap;
gap: 8rpx 16rpx;
}
.detail-kv {
display: flex;
gap: 6rpx;
min-width: 45%;
}
.detail-label {
color: var(--text-sub);
white-space: nowrap;
}
.detail-value {
color: var(--text-main);
flex: 1;
word-break: break-all;
}
.detail-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
}
.detail-index {
color: var(--text-sub);
font-size: 24rpx;
}
.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 {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 16rpx;
}
.popup-actions .ghost-action,
.popup-actions .submit {
flex: 1;
margin-top: 0;
margin-left: 0;
margin-right: 0;
width: auto;
}
.textarea {
flex: 1;
text-align: right;
font-size: 26rpx;
color: var(--text-main);
background: transparent;
min-height: 40rpx;
line-height: 40rpx;
}
.ghost-action {
text-align: center;
padding: 10rpx 16rpx;
border-radius: 999rpx;
font-weight: 800;
font-size: 26rpx;
flex: 1;
background: #F0F6FF;
color: #2563eb;
border: 1rpx solid #F0F6FF;
margin: 10rpx;
width: calc(100% - 60rpx);
display: flex;
align-items: center;
justify-content: center;
}
.rule-popup {
max-height: 70vh;
}
.rule-content {
font-size: 24rpx;
color: #374151;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
.record-meta {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 6rpx;
}
.text-red {
color: #ef4444;
}
</style>