更新日志,一番赏移除道具卡

This commit is contained in:
ty200947752 2025-12-15 11:02:37 +08:00
parent 8f044d68ca
commit b79cd37932
17 changed files with 1984 additions and 440 deletions

View File

@ -1,8 +1,12 @@
<script> <script>
export default { export default {
onLaunch: function() { onLaunch: function(options) {
console.log('App Launch') console.log('App Launch', options)
try { uni.setStorageSync('app_session_id', String(Date.now())) } catch (_) {} try { uni.setStorageSync('app_session_id', String(Date.now())) } catch (_) {}
if (options && options.query && options.query.invite_code) {
console.log('App Launch captured invite_code:', options.query.invite_code)
try { uni.setStorageSync('inviter_code', options.query.invite_code) } catch (e) { console.error('Save invite code failed', e) }
}
}, },
onShow: function() { onShow: function() {
console.log('App Show') console.log('App Show')

View File

@ -5,6 +5,10 @@ export function wechatLogin(code, invite_code) {
return request({ url: '/api/app/users/weixin/login', method: 'POST', data }) return request({ url: '/api/app/users/weixin/login', method: 'POST', data })
} }
export function getInventory(user_id, page = 1, page_size = 20){
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size } })
}
export function bindPhone(user_id, code, extraHeader = {}) { export function bindPhone(user_id, code, extraHeader = {}) {
return authRequest({ url: `/api/app/users/${user_id}/phone/bind`, method: 'POST', data: { code }, header: extraHeader }) return authRequest({ url: `/api/app/users/${user_id}/phone/bind`, method: 'POST', data: { code }, header: extraHeader })
} }
@ -66,3 +70,61 @@ export function drawActivityIssue(activity_id, issue_id) {
export function getActivityWinRecords(activity_id, page = 1, page_size = 20) { export function getActivityWinRecords(activity_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/activities/${activity_id}/wins`, method: 'GET', data: { page, page_size } }) return authRequest({ url: `/api/app/activities/${activity_id}/wins`, method: 'GET', data: { page, page_size } })
} }
export function getIssueChoices(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/choices`, method: 'GET' })
}
export function getProductDetail(product_id) {
return authRequest({ url: `/api/app/products/${product_id}`, method: 'GET' })
}
export function redeemInventory(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/redeem`, method: 'POST', data: { inventory_ids: ids } })
}
export function requestShipping(user_id, ids) {
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data: { inventory_ids: ids } })
}
export function getItemCards(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/item_cards`, method: 'GET' })
}
export function getUserCoupons(user_id, status, page = 1, page_size = 20) {
const data = { page, page_size }
if (status !== undefined) data.status = status
return authRequest({ url: `/api/app/users/${user_id}/coupons`, method: 'GET', data })
}
export function redeemCoupon(user_id, code) {
return authRequest({ url: `/api/app/users/${user_id}/coupons/redeem`, method: 'POST', data: { code } })
}
export function joinLottery(data) {
return authRequest({ url: '/api/app/lottery/join', method: 'POST', data })
}
export function createWechatOrder(data) {
return authRequest({ url: '/api/app/pay/wechat/jsapi/preorder', method: 'POST', data })
}
export function getLotteryResult(order_no) {
return authRequest({ url: '/api/app/lottery/result', method: 'GET', data: { order_no } })
}
export function getUserPoints(user_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/points`, method: 'GET', data: { page, page_size } })
}
export function redeemProductByPoints(user_id, product_id, quantity) {
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-product`, method: 'POST', data: { product_id, quantity } })
}
export function getTasks(page = 1, page_size = 20) {
return authRequest({ url: '/api/app/task-center/tasks', method: 'GET', data: { page, page_size } })
}
export function getShipments(user_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/shipments`, method: 'GET', data: { page, page_size } })
}

View File

@ -1,146 +0,0 @@
uni.api.esm.js:502 App Launch
uni.api.esm.js:502 App Show
VM75:363 Error: not node js file system!path:/saaa_config.json; go __invokeHandler__ readFile worker? false
h @ VM75:363
a.WeixinJSCore.invokeHandler @ VM75:363
b @ WAServiceMainContext.js:1
invoke @ WAServiceMainContext.js:1
invoke @ WAServiceMainContext.js:1
u @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
ae @ WAServiceMainContext.js:1
o @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
QX @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
c.emit @ WAServiceMainContext.js:1
emitInternal @ WAServiceMainContext.js:1
e @ WAServiceMainContext.js:1
c.emit @ WAServiceMainContext.js:1
emitInternal @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
_ @ WAServiceMainContext.js:1
B @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
a @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
c.emit @ WAServiceMainContext.js:1
emit @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
_emit @ WAServiceMainContext.js:1
emit @ WAServiceMainContext.js:1
emit @ WAServiceMainContext.js:1
subscribeHandler @ WAServiceMainContext.js:1
ret.subscribeHandler
Show 8 more frames
uni.api.esm.js:502 mine onShow token: isLogin: false phoneBound: false
uni.api.esm.js:502 mine login modal confirm: true
WAServiceMainContext.js:1 [wxapplib]] backgroundfetch privacy fail {"errno":101,"errMsg":"private_getBackgroundFetchData:fail private_getBackgroundFetchData:fail:jsapi invalid request data"}
(anonymous) @ WAServiceMainContext.js:1
i @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
fail @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
u @ WAServiceMainContext.js:1
fail @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
G @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
r @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
callAndRemove @ WAServiceMainContext.js:1
invokeCallbackHandler @ WAServiceMainContext.js:1
ret.invokeCallbackHandler
uni.api.esm.js:502 login_flow start getPhoneNumber, codeExists: true
uni.api.esm.js:502 login_flow uni.login success, loginCode exists: true
uni.api.esm.js:502 HTTP request POST /api/app/users/weixin/login data {code: "0f1tQZFa1a5TFK0U6UGa11gIyJ0tQZF6"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/weixin/login status 200 body {user_id: 813, nickname: "善良的巴乔", avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAA…BNCqAnHlGBRgAAQAA//8HSv0f1XCh/AAAAABJRU5ErkJggg==", invite_code: "AZCHW75Z", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODEzL…MzNH0.ZUItPpkDEbAhTy8g80PGRxLxQ_JXqS4jlu_vz7IrMAU"}avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAQMAAAD58POIAAAABlBMVEUAAABmAADpPQ5dAAAAAXRSTlMAQObYZgAAAD9JREFUeJxiAIH//xHkSBdAEofwRrjAKEAFsMQCTzQjXgCRSMDsES4AoWAALj6iBRABNCqAnHlGBRgAAQAA//8HSv0f1XCh/AAAAABJRU5ErkJggg=="invite_code: "AZCHW75Z"nickname: "善良的巴乔"token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODEzLCJ1c2VybmFtZSI6IuWWhOiJr-eahOW3tOS5lCIsIm5pY2tuYW1lIjoi5ZaE6Imv55qE5be05LmUIiwiaXNfc3VwZXIiOjAsInBsYXRmb3JtIjoiQVBQIiwiZXhwIjoxNzY1ODc3MzM0LCJuYmYiOjE3NjMyODUzMzQsImlhdCI6MTc2MzI4NTMzNH0.ZUItPpkDEbAhTy8g80PGRxLxQ_JXqS4jlu_vz7IrMAU"user_id: 813[[Prototype]]: Object
uni.api.esm.js:502 login_flow wechatLogin response user_id: 813
uni.api.esm.js:502 login_flow token stored
uni.api.esm.js:502 login_flow user_id stored: 813
uni.api.esm.js:502 login_flow bindPhone start
uni.api.esm.js:502 HTTP request POST /api/app/users/813/phone/bind data {code: "c568695e007ec9d5dca1ecaf4d5452dd378fd42542d4a90fd9ad7b942b5d4248"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/813/phone/bind status 401 body {code: 10103, message: "您的账号登录过期,请重新登录。"}
uni.api.esm.js:502 login_flow bindPhone 401, try re-login and retry
__f__ @ uni.api.esm.js:502
Ku.wu.__f__ @ mp.esm.js:523
(anonymous) @ index.vue:59
s @ app-service.js:1219
(anonymous) @ app-service.js:1219
(anonymous) @ app-service.js:1219
asyncGeneratorStep @ app-service.js:1174
i @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ index.vue:95
I.forEach.v.<computed> @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
i @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
G @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
r @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
callAndRemove @ WAServiceMainContext.js:1
invokeCallbackHandler @ WAServiceMainContext.js:1
ret.invokeCallbackHandler
uni.api.esm.js:502 HTTP request POST /api/app/users/weixin/login data {code: "0e15cAll2FVxFg4MxDnl2v9KUj15cAlO"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/weixin/login status 200 body {user_id: 813, nickname: "善良的巴乔", avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAA…BNCqAnHlGBRgAAQAA//8HSv0f1XCh/AAAAABJRU5ErkJggg==", invite_code: "AZCHW75Z", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODEzL…MzNH0.ZUItPpkDEbAhTy8g80PGRxLxQ_JXqS4jlu_vz7IrMAU"}
uni.api.esm.js:502 HTTP request POST /api/app/users/813/phone/bind data {code: "c568695e007ec9d5dca1ecaf4d5452dd378fd42542d4a90fd9ad7b942b5d4248"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/813/phone/bind status 401 body {code: 10103, message: "您的账号登录过期,请重新登录。"}
uni.api.esm.js:502 login_flow error: 您的账号登录过期,请重新登录。 status: 401
__f__ @ uni.api.esm.js:502
Ku.wu.__f__ @ mp.esm.js:523
(anonymous) @ index.vue:90
s @ app-service.js:1219
(anonymous) @ app-service.js:1219
(anonymous) @ app-service.js:1219
asyncGeneratorStep @ app-service.js:1174
i @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
i @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ index.vue:95
I.forEach.v.<computed> @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
i @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
G @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
r @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
callAndRemove @ WAServiceMainContext.js:1
invokeCallbackHandler @ WAServiceMainContext.js:1
ret.invokeCallbackHandler

View File

@ -66,7 +66,8 @@
"usingComponents" : true "usingComponents" : true
}, },
"mp-toutiao" : { "mp-toutiao" : {
"usingComponents" : true "usingComponents" : true,
"appid" : "ttf031868c6f33d91001"
}, },
"uniStatistics" : { "uniStatistics" : {
"enable" : false "enable" : false

View File

@ -21,6 +21,13 @@
} }
} }
, ,
{
"path": "pages/shop/detail",
"style": {
"navigationBarTitleText": "商品详情"
}
}
,
{ {
"path": "pages/cabinet/index", "path": "pages/cabinet/index",
"style": { "style": {
@ -97,6 +104,12 @@
{ {
"path": "pages/activity/duiduipeng/index", "path": "pages/activity/duiduipeng/index",
"style": { "navigationBarTitleText": "对对碰" } "style": { "navigationBarTitleText": "对对碰" }
},
{
"path": "pages/register/register",
"style": {
"navigationBarTitleText": ""
}
} }
], ],
"tabBar": { "tabBar": {

View File

@ -1,65 +0,0 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '对对碰' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">参与价{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail } from '../../api/appUser'
const detail = ref({})
const statusText = ref('')
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) fetchDetail(id)
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
</style>

View File

@ -5,7 +5,7 @@
</view> </view>
<view class="header"> <view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view> <view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">参与价{{ detail.price_draw }}</view> <view class="meta" v-if="detail.price_draw !== undefined">参与价{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
</view> </view>
<view class="issues" v-if="showIssues"> <view class="issues" v-if="showIssues">

View File

@ -1,65 +0,0 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '无限赏' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail } from '../../api/appUser'
const detail = ref({})
const statusText = ref('')
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) fetchDetail(id)
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
</style>

View File

@ -5,11 +5,11 @@
</view> </view>
<view class="header"> <view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view> <view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选{{ detail.price_draw }}</view> <view class="meta" v-if="detail.price_draw !== undefined">单次抽选{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
</view> </view>
<view class="draw-actions"> <view class="draw-actions">
<button class="draw-btn" @click="() => onMachineDraw(1)">单次抽选</button> <button class="draw-btn" @click="() => openPayment(1)">参加一次</button>
<button class="draw-btn" @click="() => onMachineDraw(10)">十次抽选</button> <button class="draw-btn" @click="() => openPayment(10)">参加十次</button>
<button class="draw-btn secondary" @click="onMachineTry">试一试</button> <button class="draw-btn secondary" @click="onMachineTry">试一试</button>
</view> </view>
<view class="issues" v-if="showIssues && issues.length"> <view class="issues" v-if="showIssues && issues.length">
@ -27,6 +27,13 @@
<button class="overlay-close" @tap="closeFlip">关闭</button> <button class="overlay-close" @tap="closeFlip">关闭</button>
</view> </view>
</view> </view>
<PaymentPopup
v-model:visible="paymentVisible"
:amount="paymentAmount"
:coupons="coupons"
:propCards="propCards"
@confirm="onPaymentConfirm"
/>
</template> </template>
@ -35,7 +42,8 @@ import { ref, computed, getCurrentInstance } from 'vue'
import ElCard from '../../../components/ElCard.vue' import ElCard from '../../../components/ElCard.vue'
import FlipGrid from '../../../components/FlipGrid.vue' import FlipGrid from '../../../components/FlipGrid.vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue } from '../../../api/appUser' import PaymentPopup from '../../../components/PaymentPopup.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, joinLottery, createWechatOrder, getLotteryResult, getItemCards, getUserCoupons } from '../../../api/appUser'
const detail = ref({}) const detail = ref({})
const statusText = ref('') const statusText = ref('')
@ -56,6 +64,14 @@ const currentIssueTitle = computed(() => {
const points = ref(0) const points = ref(0)
const flipRef = ref(null) const flipRef = ref(null)
const showFlip = ref(false) const showFlip = ref(false)
const paymentVisible = ref(false)
const paymentAmount = ref('0.00')
const coupons = ref([])
const propCards = ref([])
const pendingCount = ref(1)
const selectedCoupon = ref(null)
const selectedCard = ref(null)
const pricePerDrawYuan = computed(() => ((Number(detail.value.price_draw || 0) / 100) || 0))
function statusToText(s) { function statusToText(s) {
if (s === 1) return '进行中' if (s === 1) return '进行中'
@ -220,27 +236,134 @@ function onPreviewBanner() {
if (url) uni.previewImage({ urls: [url], current: url }) if (url) uni.previewImage({ urls: [url], current: url })
} }
function openPayment(count) {
const times = Math.max(1, Number(count || 1))
pendingCount.value = times
paymentAmount.value = (pricePerDrawYuan.value * times).toFixed(2)
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
})
return
}
paymentVisible.value = true
fetchPropCards()
fetchCoupons()
}
function onMachineDraw(count) { async function onPaymentConfirm(data) {
selectedCoupon.value = data && data.coupon ? data.coupon : null
selectedCard.value = data && data.card ? data.card : null
paymentVisible.value = false
await onMachineDraw(pendingCount.value)
}
async function fetchPropCards() {
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
try {
const res = await getItemCards(user_id)
let list = []
if (Array.isArray(res)) list = res
else if (res && Array.isArray(res.list)) list = res.list
else if (res && Array.isArray(res.data)) list = res.data
propCards.value = list.map((i, idx) => ({
id: i.id ?? i.card_id ?? String(idx),
name: i.name ?? i.title ?? '道具卡'
}))
} catch (e) {
propCards.value = []
}
}
async function fetchCoupons() {
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
try {
const res = await getUserCoupons(user_id, 0, 1, 100)
let list = []
if (Array.isArray(res)) list = res
else if (res && Array.isArray(res.list)) list = res.list
else if (res && Array.isArray(res.data)) list = res.data
coupons.value = list.map((i, idx) => {
const amountCents = (i.remaining !== undefined && i.remaining !== null) ? Number(i.remaining) : Number(i.amount ?? i.value ?? 0)
const amt = isNaN(amountCents) ? 0 : (amountCents / 100)
return {
id: i.id ?? i.coupon_id ?? String(idx),
name: i.name ?? i.title ?? '优惠券',
amount: Number(amt).toFixed(2)
}
})
} catch (e) {
coupons.value = []
}
}
async function onMachineDraw(count) {
showFlip.value = true showFlip.value = true
try { if (flipRef.value && flipRef.value.reset) flipRef.value.reset() } catch (_) {} try { if (flipRef.value && flipRef.value.reset) flipRef.value.reset() } catch (_) {}
const aid = activityId.value || '' const aid = activityId.value || ''
const iid = currentIssueId.value || '' const iid = currentIssueId.value || ''
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return } if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
})
return
}
const openid = uni.getStorageSync('openid')
if (!openid) { uni.showToast({ title: '缺少OpenID请重新登录', icon: 'none' }); return }
drawLoading.value = true drawLoading.value = true
const times = Math.max(1, Number(count || 1)) try {
const calls = Array(times).fill(0).map(() => drawActivityIssue(aid, iid)) const times = Math.max(1, Number(count || 1))
Promise.allSettled(calls).then(list => { const joinRes = await joinLottery({
drawLoading.value = false activity_id: Number(aid),
const items = list.map(r => { issue_id: Number(iid),
const obj = r.status === 'fulfilled' ? r.value : {} channel: 'miniapp',
const data = obj && (obj.data || obj.result || obj.reward || obj.item || obj) count: times,
const title = String((data && (data.title || data.name || data.product_name)) || '未知奖励') coupon_id: selectedCoupon.value && selectedCoupon.value.id ? Number(selectedCoupon.value.id) : 0,
const image = String((data && (data.image || data.img || data.pic || data.product_image)) || '') item_card_id: selectedCard.value && selectedCard.value.id ? Number(selectedCard.value.id) : 0
})
const orderNo = joinRes && (joinRes.order_no || joinRes.data?.order_no || joinRes.result?.order_no)
if (!orderNo) throw new Error('未获取到订单号')
const payRes = await createWechatOrder({ openid, order_no: orderNo })
await new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: payRes.timeStamp || payRes.timestamp,
nonceStr: payRes.nonceStr || payRes.noncestr,
package: payRes.package,
signType: payRes.signType || 'MD5',
paySign: payRes.paySign,
success: resolve,
fail: reject
})
})
const resultRes = await getLotteryResult(orderNo)
const raw = resultRes && (resultRes.list || resultRes.items || resultRes.data || resultRes.result || resultRes)
const arr = Array.isArray(raw) ? raw : (Array.isArray(resultRes?.data) ? resultRes.data : [raw])
const items = arr.filter(Boolean).map(d => {
const title = String((d && (d.title || d.name || d.product_name)) || '奖励')
const image = String((d && (d.image || d.img || d.pic || d.product_image)) || '')
return { title, image } return { title, image }
}) })
if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(items) if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(items)
}).catch(() => { drawLoading.value = false; if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults([{ title: '抽选失败', image: '' }]) }) } catch (e) {
if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults([{ title: e.message || '抽选失败', image: '' }])
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
} finally {
drawLoading.value = false
}
} }
function onMachineTry() { function onMachineTry() {

View File

@ -1,66 +0,0 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '一番赏' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">抽选价{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail } from '../../api/appUser'
const detail = ref({})
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
const statusText = ref('')
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) fetchDetail(id)
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
</style>

View File

@ -5,13 +5,9 @@
</view> </view>
<view class="header"> <view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view> <view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">抽选价{{ detail.price_draw }}</view> <view class="meta" v-if="detail.price_draw !== undefined">抽选价{{ (Number(detail.price_draw || 0) / 100).toFixed(2) }}</view>
</view>
<view class="draw-actions">
<button class="draw-btn" @click="() => onMachineDraw(1)">单次抽选</button>
<button class="draw-btn" @click="() => onMachineDraw(10)">十次抽选</button>
<button class="draw-btn secondary" @click="onMachineTry">试一试</button>
</view> </view>
<view class="issues" v-if="showIssues && issues.length"> <view class="issues" v-if="showIssues && issues.length">
<view class="issue-switch"> <view class="issue-switch">
<button class="switch-btn" @click="prevIssue"></button> <button class="switch-btn" @click="prevIssue"></button>
@ -19,6 +15,17 @@
<button class="switch-btn" @click="nextIssue"></button> <button class="switch-btn" @click="nextIssue"></button>
</view> </view>
</view> </view>
<!-- 引入位置选择组件 -->
<view class="selector-section" v-if="activityId && currentIssueId">
<YifanSelector
:activity-id="activityId"
:issue-id="currentIssueId"
:price-per-draw="Number(detail.price_draw || 0) / 100"
@payment-success="onPaymentSuccess"
/>
</view>
</scroll-view> </scroll-view>
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent> <view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
<view class="flip-mask" @tap="closeFlip"></view> <view class="flip-mask" @tap="closeFlip"></view>
@ -35,7 +42,8 @@ import { ref, computed, getCurrentInstance } from 'vue'
import ElCard from '../../../components/ElCard.vue' import ElCard from '../../../components/ElCard.vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import FlipGrid from '../../../components/FlipGrid.vue' import FlipGrid from '../../../components/FlipGrid.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, drawActivityIssue, getActivityWinRecords } from '../../../api/appUser' import YifanSelector from '@/components/YifanSelector.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getActivityWinRecords } from '../../../api/appUser'
const detail = ref({}) const detail = ref({})
const issues = ref([]) const issues = ref([])
@ -242,34 +250,38 @@ function onPreviewBanner() {
} }
function onMachineDraw(count) { function onPaymentSuccess(payload) {
console.log('Payment Success:', payload)
const result = payload.result
let wonItems = []
//
if (Array.isArray(result)) {
wonItems = result
} else if (result && Array.isArray(result.list)) {
wonItems = result.list
} else if (result && Array.isArray(result.data)) {
wonItems = result.data
} else if (result && Array.isArray(result.rewards)) {
wonItems = result.rewards
} else {
//
wonItems = result ? [result] : []
}
const items = wonItems.map(data => {
const title = String((data && (data.title || data.name || data.product_name || data.reward_name)) || '未知奖励')
const image = String((data && (data.image || data.img || data.pic || data.product_image || data.reward_image)) || '')
return { title, image }
})
showFlip.value = true showFlip.value = true
try { if (flipRef.value && flipRef.value.reset) flipRef.value.reset() } catch (_) {} try { if (flipRef.value && flipRef.value.reset) flipRef.value.reset() } catch (_) {}
const aid = activityId.value || ''
const iid = currentIssueId.value || '' setTimeout(() => {
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
drawLoading.value = true
const times = Math.max(1, Number(count || 1))
const calls = Array(times).fill(0).map(() => drawActivityIssue(aid, iid))
Promise.allSettled(calls).then(list => {
drawLoading.value = false
const items = list.map(r => {
const obj = r.status === 'fulfilled' ? r.value : {}
const data = obj && (obj.data || obj.result || obj.reward || obj.item || obj)
const title = String((data && (data.title || data.name || data.product_name)) || '未知奖励')
const image = String((data && (data.image || data.img || data.pic || data.product_image)) || '')
return { title, image }
})
if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(items) if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults(items)
}).catch(() => { drawLoading.value = false; if (flipRef.value && flipRef.value.revealResults) flipRef.value.revealResults([{ title: '抽选失败', image: '' }]) }) }, 100)
}
function onMachineTry() {
const list = rewardsMap.value[currentIssueId.value] || []
if (!list.length) { uni.showToast({ title: '暂无奖池', icon: 'none' }); return }
const idx = Math.floor(Math.random() * list.length)
const it = list[idx]
uni.showModal({ title: '试一试', content: it.title || '随机预览', showCancel: false, success: () => { if (it.image) uni.previewImage({ urls: [it.image], current: it.image }) } })
} }
onLoad((opts) => { onLoad((opts) => {
@ -307,7 +319,19 @@ function closeFlip() { showFlip.value = false }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx } .issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { } .issues-list { }
.issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx } .issue-switch { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin: 0 24rpx 24rpx }
.switch-btn { width: 72rpx; height: 72rpx; border-radius: 999rpx; background: #fff3df; border: 2rpx solid #f0c58a; color: #8a5a2b } .switch-btn {
width: 72rpx;
height: 72rpx;
border-radius: 999rpx;
background: #fff3df;
border: 2rpx solid #f0c58a;
color: #8a5a2b;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
}
.issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx } .issue-title { font-size: 28rpx; color: #6b4b1f; background: #ffdfaa; border-radius: 12rpx; padding: 8rpx 16rpx }
.reward { display: flex; align-items: center; margin-bottom: 8rpx } .reward { display: flex; align-items: center; margin-bottom: 8rpx }
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 } .reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }

View File

@ -1,13 +1,123 @@
<template> <template>
<view class="wrap">货柜</view> <view class="wrap">
<!-- 顶部 Tab -->
<view class="tabs">
<view class="tab-item" :class="{ active: currentTab === 0 }" @tap="switchTab(0)">
<text class="tab-text">待处理</text>
<text class="tab-count" v-if="aggregatedList.length > 0">({{ aggregatedList.length }})</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 1 }" @tap="switchTab(1)">
<text class="tab-text">已申请发货</text>
<text class="tab-count" v-if="shippedList.length > 0">({{ shippedList.length }})</text>
</view>
</view>
<!-- Tab 0: 待处理商品 -->
<block v-if="currentTab === 0">
<!-- 全选栏 -->
<view class="action-bar" v-if="aggregatedList.length > 0">
<view class="select-all" @tap="toggleSelectAll">
<view class="checkbox" :class="{ checked: isAllSelected }"></view>
<text>全选</text>
</view>
</view>
<view v-if="loading && aggregatedList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!aggregatedList || aggregatedList.length === 0" class="status-text">背包空空如也</view>
<view v-else class="inventory-grid">
<view v-for="(item, index) in aggregatedList" :key="index" class="inventory-item">
<view class="checkbox-area" @tap.stop="toggleSelect(item)">
<view class="checkbox" :class="{ checked: item.selected }"></view>
</view>
<image :src="item.image" mode="aspectFill" class="item-image" @error="onImageError(index, 'aggregated')" />
<view class="item-info">
<text class="item-name">{{ item.name || '未命名道具' }}</text>
<text class="item-price" v-if="item.price">单价: ¥{{ item.price }}</text>
<view class="item-actions">
<text class="item-count" v-if="!item.selected">x{{ item.count || 1 }}</text>
<view class="stepper" v-else @tap.stop>
<text class="step-btn minus" @tap.stop="changeCount(item, -1)">-</text>
<text class="step-num">{{ item.selectedCount }}</text>
<text class="step-btn plus" @tap.stop="changeCount(item, 1)">+</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="loading && aggregatedList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!hasMore && aggregatedList.length > 0" class="no-more">没有更多了</view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="hasSelected">
<view class="selected-info">已选 {{ totalSelectedCount }} </view>
<view class="btn-group">
<button class="action-btn btn-ship" @tap="onShip">发货</button>
<button class="action-btn btn-redeem" @tap="onRedeem">兑换</button>
</view>
</view>
<view class="bottom-spacer" v-if="hasSelected"></view>
</block>
<!-- Tab 1: 已申请发货 -->
<block v-if="currentTab === 1">
<view v-if="loading && shippedList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!shippedList || shippedList.length === 0" class="status-text">暂无已发货记录</view>
<view v-else class="inventory-grid">
<view v-for="(item, index) in shippedList" :key="index" class="inventory-item">
<!-- 已发货仅展示 -->
<image :src="item.image" mode="aspectFill" class="item-image" @error="onImageError(index, 'shipped')" />
<view class="item-info">
<text class="item-name">{{ item.name || '未命名道具' }}</text>
<text class="item-count">x{{ item.count || 1 }}</text>
<text class="item-status">已申请发货</text>
<text class="item-meta" v-if="item.express_code || item.express_no">快递{{ item.express_code }} {{ item.express_no }}</text>
<text class="item-meta" v-if="item.shipped_at">发货时间{{ formatDate(item.shipped_at) }}</text>
<text class="item-meta" v-if="item.received_at">签收时间{{ formatDate(item.received_at) }}</text>
</view>
</view>
</view>
<view v-if="loading && shippedList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!hasMore && shippedList.length > 0" class="no-more">没有更多了</view>
</block>
</view>
</template> </template>
<script setup> <script setup>
import { onShow } from '@dcloudio/uni-app' import { ref, computed } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import { getInventory, getProductDetail, redeemInventory, requestShipping, listAddresses, getShipments } from '@/api/appUser'
const currentTab = ref(0)
const aggregatedList = ref([])
const shippedList = ref([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(100)
const hasMore = ref(true)
const totalCount = computed(() => {
return aggregatedList.value.reduce((sum, item) => sum + (item.count || 1), 0)
})
const hasSelected = computed(() => {
return aggregatedList.value.some(item => item.selected)
})
const totalSelectedCount = computed(() => {
return aggregatedList.value.reduce((sum, item) => sum + (item.selected ? item.selectedCount : 0), 0)
})
const isAllSelected = computed(() => {
return aggregatedList.value.length > 0 && aggregatedList.value.every(item => item.selected)
})
onShow(() => { onShow(() => {
const token = uni.getStorageSync('token') const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound') const phoneBound = !!uni.getStorageSync('phone_bound')
console.log('cabinet onShow token:', token, 'isLogin:', !!token, 'phoneBound:', phoneBound) console.log('cabinet onShow token:', token, 'isLogin:', !!token, 'phoneBound:', phoneBound)
if (!token || !phoneBound) { if (!token || !phoneBound) {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
@ -19,10 +129,686 @@ onShow(() => {
} }
} }
}) })
return
}
//
page.value = 1
hasMore.value = true
aggregatedList.value = []
shippedList.value = []
const uid = uni.getStorageSync("user_id")
if (currentTab.value === 1) {
loadShipments(uid)
} else {
loadAllInventory(uid)
} }
}) })
onReachBottom(() => {
if (hasMore.value && !loading.value) {
const uid = uni.getStorageSync("user_id")
if (currentTab.value === 1) {
loadShipments(uid)
} else {
loadInventory(uid)
}
}
})
function switchTab(index) {
currentTab.value = index
//
page.value = 1
hasMore.value = true
aggregatedList.value = []
shippedList.value = []
const uid = uni.getStorageSync("user_id")
if (currentTab.value === 1) {
loadShipments(uid)
} else {
loadAllInventory(uid)
}
}
function cleanUrl(u) {
if (!u) return '/static/logo.png'
let s = String(u).trim()
// JSON
if (s.startsWith('[') && s.endsWith(']')) {
try {
const arr = JSON.parse(s)
if (Array.isArray(arr) && arr.length > 0) {
s = arr[0]
}
} catch (e) {
console.warn('JSON parse failed for image:', s)
}
}
//
s = s.replace(/[`'"]/g, '').trim()
// http
const m = s.match(/https?:\/\/[^\s]+/)
if (m && m[0]) return m[0]
return s || '/static/logo.png'
}
function onImageError(index, type = 'aggregated') {
if (type === 'aggregated' && aggregatedList.value[index]) {
aggregatedList.value[index].image = '/static/logo.png'
} else if (type === 'shipped' && shippedList.value[index]) {
shippedList.value[index].image = '/static/logo.png'
}
}
function formatDate(dateStr) {
if (!dateStr) return ''
const d = new Date(dateStr)
if (isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const da = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const min = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${da} ${h}:${min}`
}
async function loadShipments(uid) {
if (loading.value) return
loading.value = true
try {
const res = await getShipments(uid, page.value, pageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
const mapped = list.map(s => ({
image: '/static/logo.png',
name: '发货单',
count: s.count ?? (Array.isArray(s.inventory_ids) ? s.inventory_ids.length : 0),
express_code: s.express_code || '',
express_no: s.express_no || '',
shipped_at: s.shipped_at || '',
received_at: s.received_at || '',
status: s.status
}))
const next = page.value === 1 ? mapped : [...shippedList.value, ...mapped]
shippedList.value = next
if (list.length < pageSize.value || (page.value * pageSize.value >= total && total > 0)) { hasMore.value = false } else { page.value += 1 }
if (list.length === 0) { hasMore.value = false }
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
async function loadInventory(uid) {
if (loading.value) return
loading.value = true
try {
const res = await getInventory(uid, page.value, pageSize.value)
console.log('Inventory loaded:', res)
let list = []
let total = 0
if (res && Array.isArray(res.list)) {
list = res.list
total = res.total || 0
} else if (res && Array.isArray(res.data)) { // data: []
list = res.data
total = res.total || 0
} else if (Array.isArray(res)) { //
list = res
total = res.length
}
// status=1 () status=3 (使//)
// status=1:
// status=3:
const filteredList = list.filter(item => {
const s = Number(item.status)
return s === 1 || s === 3
})
//
if (filteredList.length > 0) {
console.log('Debug Inventory Item:', filteredList[0])
}
// Tab
const targetItems = filteredList.filter(item => {
// Tab 0: (has_shipment false status=1)
// Tab 1: (has_shipment true)
// API has_shipment true/false 1/0 "true"/"false"
//
const isShipped = item.has_shipment === true || item.has_shipment === 1 || String(item.has_shipment) === 'true' || String(item.has_shipment) === '1'
if (currentTab.value === 1) {
//
// status=3 has_shipment=true
return isShipped
} else {
// status=1 (status=3 )
return !isShipped && Number(item.status) === 1
}
})
console.log('Filtered list (status=1, tab=' + currentTab.value + '):', targetItems)
//
const newItems = targetItems.map(item => {
let imageUrl = ''
try {
let rawImg = item.product_images || item.image
if (rawImg && typeof rawImg === 'string') {
imageUrl = cleanUrl(rawImg)
}
} catch (e) {
console.error('Image parse error:', e)
}
const isShipped = item.has_shipment === true || item.has_shipment === 1 || String(item.has_shipment) === 'true' || String(item.has_shipment) === '1'
return {
id: item.product_id || item.id, // 使 product_id
original_ids: [item.id], // id
name: (item.product_name || item.name || '').trim(),
image: imageUrl,
count: 1,
selected: false,
selectedCount: 1,
has_shipment: isShipped,
updated_at: item.updated_at //
}
})
console.log('Mapped new items:', newItems.length)
//
// 1. newItems
// 2. newItems
//
let currentList = currentTab.value === 1 ? shippedList : aggregatedList
let next = page.value === 1 ? [] : [...currentList.value]
if (currentTab.value === 1) {
// updated_at
// ID
// UI computed
// flat list updated_at + product_id
// updated_at
// updated_at product_id
newItems.forEach(newItem => {
// updated_at product_id
// updated_at ISO
const existingItem = next.find(i =>
i.id == newItem.id &&
new Date(i.updated_at).getTime() === new Date(newItem.updated_at).getTime()
)
if (existingItem) {
existingItem.count += 1
if (Array.isArray(existingItem.original_ids)) {
existingItem.original_ids.push(...newItem.original_ids)
}
} else {
next.push(newItem)
}
})
} else {
// product_id (id)
newItems.forEach(newItem => {
if (!newItem.id) {
next.push(newItem)
return
}
const existingItem = next.find(i => i.id == newItem.id)
if (existingItem) {
existingItem.count += 1
if (Array.isArray(existingItem.original_ids)) {
existingItem.original_ids.push(...newItem.original_ids)
} else {
existingItem.original_ids = [...newItem.original_ids]
}
} else {
next.push(newItem)
}
})
}
console.log('Final aggregated list:', JSON.parse(JSON.stringify(next)))
if (currentTab.value === 1) {
shippedList.value = next
} else {
aggregatedList.value = next
}
//
// total status=1
//
if (list.length < pageSize.value || (page.value * pageSize.value >= total && total > 0)) {
hasMore.value = false
} else {
page.value += 1
}
// total 0
if (list.length === 0) {
hasMore.value = false
}
} catch (error) {
console.error('Failed to load inventory:', error)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
async function loadAllInventory(uid) {
try {
while (hasMore.value) {
await loadInventory(uid)
}
fetchProductPrices()
} catch (e) {}
}
async function fetchProductPrices() {
const currentList = currentTab.value === 1 ? shippedList : aggregatedList
const list = currentList.value
for (let i = 0; i < list.length; i++) {
const item = list[i]
if (item.id && !item.price) {
try {
const res = await getProductDetail(item.id)
if (res && (res.price !== undefined || res.data?.price !== undefined)) {
// res.price res.data.price ()
const raw = res.price !== undefined ? res.price : res.data?.price
const num = Number(raw)
item.price = isNaN(num) ? null : (num / 100)
}
} catch (e) {
console.error('Fetch price failed for:', item.id, e)
}
}
}
}
function toggleSelect(item) {
item.selected = !item.selected
if (item.selected) {
//
item.selectedCount = item.count
}
}
function toggleSelectAll() {
const newState = !isAllSelected.value
aggregatedList.value.forEach(item => {
item.selected = newState
if (newState) {
item.selectedCount = item.count
}
})
}
function changeCount(item, delta) {
if (!item.selected) return
const newCount = item.selectedCount + delta
if (newCount >= 1 && newCount <= item.count) {
item.selectedCount = newCount
}
}
async function onRedeem() {
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
const selectedItems = aggregatedList.value.filter(item => item.selected)
if (selectedItems.length === 0) return
// inventory id
let allIds = []
selectedItems.forEach(item => {
// original_ids
if (item.original_ids && item.original_ids.length >= item.selectedCount) {
// selectedCount id
const idsToRedeem = item.original_ids.slice(0, item.selectedCount)
allIds.push(...idsToRedeem)
}
})
if (allIds.length === 0) {
uni.showToast({ title: '选择无效', icon: 'none' })
return
}
uni.showModal({
title: '确认兑换',
content: `确定要兑换选中的 ${allIds.length} 件物品吗?此操作不可撤销。`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '处理中...' })
try {
await redeemInventory(user_id, allIds)
uni.showToast({ title: '兑换成功', icon: 'success' })
//
aggregatedList.value = []
page.value = 1
hasMore.value = true
loadAllInventory(user_id)
} catch (e) {
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
}
})
}
async function onShip() {
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
const selectedItems = aggregatedList.value.filter(item => item.selected)
if (selectedItems.length === 0) return
// inventory id
let allIds = []
selectedItems.forEach(item => {
if (item.original_ids && item.original_ids.length >= item.selectedCount) {
const idsToShip = item.original_ids.slice(0, item.selectedCount)
allIds.push(...idsToShip)
}
})
if (allIds.length === 0) {
uni.showToast({ title: '选择无效', icon: 'none' })
return
}
// 2.
uni.showModal({
title: '确认发货',
content: `${allIds.length} 件物品,确认申请发货?`,
confirmText: '确认发货',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '提交中...' })
try {
await requestShipping(user_id, allIds)
uni.showToast({ title: '申请成功', icon: 'success' })
//
aggregatedList.value = []
page.value = 1
hasMore.value = true
loadAllInventory(user_id)
} catch (e) {
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
}
})
}
</script> </script>
<style scoped> <style scoped>
.wrap { padding: 40rpx } .item-status {
</style> font-size: 24rpx;
color: #007AFF;
margin-top: 4rpx;
}
.item-meta { font-size: 22rpx; color: #666; margin-top: 4rpx }
.wrap { padding: 30rpx; }
.tabs {
display: flex;
background: #f5f5f5;
border-radius: 16rpx;
padding: 8rpx;
margin-bottom: 20rpx;
}
.action-bar {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding: 0 10rpx;
}
.select-all {
display: flex;
align-items: center;
font-size: 28rpx;
color: #333;
}
.select-all .checkbox {
margin-right: 12rpx;
}
.tab-item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0;
border-radius: 12rpx;
transition: all 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
gap: 8rpx;
}
.tab-item.active {
background: #fff;
color: #007AFF;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.tab-text {
font-size: 28rpx;
}
.tab-count {
font-size: 24rpx;
opacity: 0.8;
}
.header { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
.status-text { text-align: center; color: #999; margin-top: 100rpx; }
.inventory-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.inventory-item {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
display: flex;
flex-direction: row;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}
.item-image {
width: 120rpx;
height: 120rpx;
margin-right: 24rpx;
margin-bottom: 0;
border-radius: 8rpx;
background-color: #f5f5f5;
flex-shrink: 0;
}
.item-info {
flex: 1;
text-align: left;
display: flex;
flex-direction: column;
justify-content: center;
}
.item-name {
font-size: 26rpx;
color: #333;
display: block;
margin-bottom: 4rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-count {
font-size: 24rpx;
color: #999;
margin-bottom: 4rpx;
}
.item-price {
font-size: 24rpx;
color: #ff4d4f;
}
.checkbox-area {
padding: 10rpx 20rpx 10rpx 0;
display: flex;
align-items: center;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ccc;
border-radius: 50%;
position: relative;
}
.checkbox.checked {
background-color: #007AFF;
border-color: #007AFF;
}
.checkbox.checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%) rotate(45deg);
width: 10rpx;
height: 20rpx;
border-right: 4rpx solid #fff;
border-bottom: 4rpx solid #fff;
}
.item-actions {
margin-top: 10rpx;
display: flex;
align-items: center;
}
.stepper {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 8rpx;
height: 48rpx;
}
.step-btn {
width: 48rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
font-size: 32rpx;
color: #666;
}
.step-btn.minus { border-right: 1px solid #ddd; }
.step-btn.plus { border-left: 1px solid #ddd; }
.step-num {
width: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
z-index: 100;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.selected-info {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.btn-group {
display: flex;
gap: 20rpx;
}
.action-btn {
margin: 0;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
border-radius: 32rpx;
padding: 0 40rpx;
}
.btn-ship {
background-color: #f0ad4e;
color: #fff;
}
.btn-redeem {
background-color: #dd524d;
color: #fff;
}
.bottom-spacer {
height: 120rpx;
height: calc(120rpx + constant(safe-area-inset-bottom));
height: calc(120rpx + env(safe-area-inset-bottom));
}
</style>

View File

@ -173,7 +173,7 @@ export default {
const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? '' const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? ''
if (base) return base if (base) return base
const cat = i.category_name ?? i.categoryName ?? '' const cat = i.category_name ?? i.categoryName ?? ''
const price = (i.price_draw !== undefined && i.price_draw !== null) ? `${i.price_draw}` : '' const price = (i.price_draw !== undefined && i.price_draw !== null) ? `${(Number(i.price_draw || 0) / 100).toFixed(2)}` : ''
const parts = [cat, price].filter(Boolean) const parts = [cat, price].filter(Boolean)
return parts.join(' · ') return parts.join(' · ')
}, },

View File

@ -1,9 +1,50 @@
<template> <template>
<view class="container"> <view class="container">
<image class="logo" src="/static/logo.png" mode="widthFix"></image> <image class="logo" src="/static/logo.png" mode="widthFix"></image>
<view class="title">微信登录</view> <!-- #ifdef MP-TOUTIAO -->
<view class="login-form">
<view class="input-row">
<text class="label">账号</text>
<input
type="text"
v-model="account"
class="input-field"
placeholder="请输入账号"
/>
</view>
<view class="input-row">
<text class="label">密码</text>
<input
type="password"
v-model="pwd"
class="input-field"
placeholder="请输入密码"
/>
</view>
<!-- 记住账号密码 -->
<view class="remember-row">
<checkbox :value="remember" @change="handleRememberChange" />
<text class="remember-text">记住账号密码</text>
</view>
<!-- 按钮区域 -->
<view class="button-group">
<button class="btn login-btn" @click="handleLogin">登录</button>
</view>
<!-- 注册链接 -->
<view class="register-link">
<text class="register-text" @click="goToRegister">还没有账号点击注册</text>
</view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<view class="title">微信登录</view>
<button class="btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">授权手机号快速登录</button> <button class="btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">授权手机号快速登录</button>
<!-- #endif --> <!-- #endif -->
<view class="agreements"> <view class="agreements">
@ -18,15 +59,39 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed,onMounted } from 'vue'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser' import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const needBindPhone = ref(false) const needBindPhone = ref(false)
const account =ref("")
const pwd = ref ("")
const remember=ref(false)
const loggedIn = computed(() => !!uni.getStorageSync('token')) const loggedIn = computed(() => !!uni.getStorageSync('token'))
onMounted(() => {
try {
const saved = uni.getStorageSync('loginInfo')
if (saved && saved.account && saved.pwd) {
account.value = saved.account
pwd.value = saved.pwd
remember.value = true
}
} catch (e) {
console.error('读取本地登录信息失败', e)
}
})
//
const handleRememberChange = (e) => {
remember.value = e.detail.value.length > 0
}
function goToRegister() { uni.navigateTo({ url: '/pages/register/register' }) }
function onLogin() {} function onLogin() {}
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) } function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
@ -48,13 +113,16 @@ function onGetPhoneNumber(e) {
try { try {
const loginCode = res.code const loginCode = res.code
console.log('login_flow uni.login success, loginCode exists:', !!loginCode) console.log('login_flow uni.login success, loginCode exists:', !!loginCode)
const data = await wechatLogin(loginCode) const inviterCode = uni.getStorageSync('inviter_code')
console.log('login_flow using inviter_code:', inviterCode)
const data = await wechatLogin(loginCode, inviterCode)
console.log('login_flow wechatLogin response user_id:', data && data.user_id) console.log('login_flow wechatLogin response user_id:', data && data.user_id)
const token = data && data.token const token = data && data.token
const user_id = data && data.user_id const user_id = data && data.user_id
const avatar = data && data.avatar const avatar = data && data.avatar
const nickname = data && data.nickname const nickname = data && data.nickname
const invite_code = data && data.invite_code const invite_code = data && data.invite_code
const openid = data && data.openid
uni.setStorageSync('user_info', data || {}) uni.setStorageSync('user_info', data || {})
if (token) { if (token) {
uni.setStorageSync('token', token) uni.setStorageSync('token', token)
@ -73,6 +141,9 @@ function onGetPhoneNumber(e) {
if (invite_code) { if (invite_code) {
uni.setStorageSync('invite_code', invite_code) uni.setStorageSync('invite_code', invite_code)
} }
if (openid) {
uni.setStorageSync('openid', openid)
}
console.log('login_flow bindPhone start') console.log('login_flow bindPhone start')
try { try {
// token // token
@ -87,7 +158,7 @@ function onGetPhoneNumber(e) {
const relogin = await new Promise((resolve, reject) => { const relogin = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject }) uni.login({ provider: 'weixin', success: resolve, fail: reject })
}) })
const data2 = await wechatLogin(relogin.code) const data2 = await wechatLogin(relogin.code, inviterCode)
const token2 = data2 && data2.token const token2 = data2 && data2.token
const user2 = data2 && data2.user_id const user2 = data2 && data2.user_id
if (token2) uni.setStorageSync('token', token2) if (token2) uni.setStorageSync('token', token2)
@ -134,6 +205,56 @@ function onGetPhoneNumber(e) {
</script> </script>
<style scoped> <style scoped>
.login-form {
padding: 20px;
}
.input-row {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.remember-row {
display: flex;
align-items: center;
margin-bottom: 60rpx;
}
.remember-text {
margin-left: 16rpx;
font-size: 28rpx;
color: #666;
}
.register-link {
text-align: center;
}
.register-text {
font-size: 26rpx;
color: #999;
text-decoration: underline; /* 可选:加下划线 */
}
.register-text:active {
color: #ff6700; /* 点击时变色 */
}
.label {
width: 60px; /* 固定宽度,保证对齐 */
font-size: 16px;
color: #333;
}
.input-field {
flex: 1;
height: 40px;
border: 2px solid #ccc; /* 边框加粗 */
border-radius: 4px;
padding: 0 10px;
font-size: 16px;
/* 去除 iOS 默认样式 */
-webkit-appearance: none;
outline: none;
}
.container { padding: 40rpx; display: flex; flex-direction: column; align-items: center } .container { padding: 40rpx; display: flex; flex-direction: column; align-items: center }
.logo { width: 200rpx; margin-top: 100rpx; margin-bottom: 40rpx } .logo { width: 200rpx; margin-top: 100rpx; margin-bottom: 40rpx }
.title { font-size: 36rpx; margin-bottom: 20rpx } .title { font-size: 36rpx; margin-bottom: 20rpx }

View File

@ -6,6 +6,9 @@
<view class="nickname">{{ nickname || '未登录' }}</view> <view class="nickname">{{ nickname || '未登录' }}</view>
<view class="userid">ID{{ userId || '-' }}</view> <view class="userid">ID{{ userId || '-' }}</view>
</view> </view>
<view class="refresh-btn" @click="refresh" :class="{ 'loading': loading }">
<text class="refresh-icon"></text>
</view>
</view> </view>
<view class="info"> <view class="info">
<view class="info-item"> <view class="info-item">
@ -14,24 +17,26 @@
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="info-label">邀请码</text> <text class="info-label">邀请码</text>
<text class="info-value">{{ inviteCode || '-' }}</text> <view class="invite-value-wrapper">
<text class="info-value">{{ inviteCode || '-' }}</text>
<button class="share-btn" open-type="share" v-if="inviteCode">分享</button>
</view>
</view> </view>
</view> </view>
<view class="stats"> <view class="stats">
<view class="stat"> <view class="stat" @click="showPointsPopup">
<view class="stat-label" @click="toPoints">积分</view> <view class="stat-label">积分</view>
<view class="stat-value">{{ pointsBalance }}</view> <view class="stat-value">{{ pointsBalance }}</view>
</view> </view>
<view class="stat"> <view class="stat" @click="showCouponsPopup">
<view class="stat-label"></view> <view class="stat-label">优惠</view>
<view class="stat-value">{{ stats.coupon_count || 0 }}</view> <view class="stat-value">{{ stats.coupon_count || 0 }}</view>
</view> </view>
<view class="stat"> <view class="stat" @click="showItemCardsPopup">
<view class="stat-label">道具卡</view> <view class="stat-label">道具卡</view>
<view class="stat-value">{{ stats.item_card_count || 0 }}</view> <view class="stat-value">{{ stats.item_card_count || 0 }}</view>
</view> </view>
</view> </view>
<button class="refresh" @click="refresh" :disabled="loading">刷新数据</button>
<view v-if="error" class="error">{{ error }}</view> <view v-if="error" class="error">{{ error }}</view>
</view> </view>
<view class="orders"> <view class="orders">
@ -59,12 +64,160 @@
<text class="addresses-arrow"></text> <text class="addresses-arrow"></text>
</view> </view>
</view> </view>
<view class="addresses">
<view class="addresses-title">任务中心</view>
<view class="addresses-entry" @click="showTasksPopup">
<text class="addresses-text">查看我的任务</text>
<text class="addresses-arrow"></text>
</view>
</view>
<!-- 积分明细弹窗 -->
<view class="popup-mask" v-if="pointsVisible" @tap="closePointsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">积分明细</text>
<text class="close-btn" @tap="closePointsPopup">×</text>
</view>
<scroll-view scroll-y class="points-list" @scrolltolower="loadMorePoints">
<view v-if="pointsLoading && pointsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!pointsList || pointsList.length === 0" class="status-text">暂无积分记录</view>
<view v-for="(item, index) in pointsList" :key="index" class="point-item">
<view class="point-left">
<text class="point-desc">{{ getActionText(item.action) }}</text>
<text class="point-time">{{ formatDate(item.created_at) }}</text>
</view>
<view class="point-right">
<text class="point-amount" :class="{ 'positive': Number(item.points) > 0, 'negative': Number(item.points) < 0 }">
{{ Number(item.points) > 0 ? '+' : '' }}{{ item.points }}
</text>
</view>
</view>
<view v-if="pointsLoading && pointsList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!pointsHasMore && pointsList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
<!-- 优惠券弹窗 -->
<view class="popup-mask" v-if="couponsVisible" @tap="closeCouponsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的优惠券</text>
<text class="close-btn" @tap="closeCouponsPopup">×</text>
</view>
<view class="popup-tabs">
<view class="popup-tab" :class="{ active: couponsTab === 0 }" @tap="switchCouponsTab(0)">未使用</view>
<view class="popup-tab" :class="{ active: couponsTab === 1 }" @tap="switchCouponsTab(1)">已使用</view>
<view class="popup-tab" :class="{ active: couponsTab === 2 }" @tap="switchCouponsTab(2)">去兑换</view>
</view>
<!-- 兑换界面 -->
<view v-if="couponsTab === 2" class="redeem-container">
<input class="redeem-input" v-model="redeemCode" placeholder="请输入兑换码" />
<button class="redeem-btn" @tap="handleRedeem">立即兑换</button>
</view>
<!-- 列表界面 -->
<scroll-view v-else scroll-y class="points-list" @scrolltolower="loadMoreCoupons">
<view v-if="couponsLoading && couponsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!couponsList || couponsList.length === 0" class="status-text">暂无优惠券</view>
<view v-for="(item, index) in couponsList" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-name">{{ item.name || '优惠券' }}</text>
<text class="coupon-desc">{{ item.rules || item.description || '' }}</text>
<text class="coupon-time">有效期至{{ formatDate(item.valid_end || item.end_time) }}</text>
</view>
<view class="coupon-right">
<view v-if="couponsTab === 0 && (item.remaining !== undefined && item.remaining !== null)" class="coupon-amount-wrapper">
<text class="symbol">¥</text>
<text class="amount-value">{{ (Number(item.remaining || 0) / 100).toFixed(2) }}</text>
</view>
<text class="coupon-status">{{ couponsTab === 0 ? '未使用' : '已使用/过期' }}</text>
</view>
</view>
<view v-if="couponsLoading && couponsList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!couponsHasMore && couponsList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
<!-- 道具卡弹窗 -->
<view class="popup-mask" v-if="itemCardsVisible" @tap="closeItemCardsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的道具卡</text>
<text class="close-btn" @tap="closeItemCardsPopup">×</text>
</view>
<scroll-view scroll-y class="points-list">
<view v-if="itemCardsLoading && itemCardsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!itemCardsList || itemCardsList.length === 0" class="status-text">暂无道具卡</view>
<view v-for="(item, index) in itemCardsList" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-name">{{ item.name || item.title || '道具卡' }}</text>
<text class="coupon-desc">{{ item.description || item.rules || '' }}</text>
<text class="coupon-time" v-if="item.valid_end || item.end_time">有效期至{{ formatDate(item.valid_end || item.end_time) }}</text>
</view>
<view class="coupon-right">
<text class="coupon-status">数量{{ item.remaining ?? item.count ?? 1 }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 任务中心弹窗 -->
<view class="popup-mask" v-if="tasksVisible" @tap="closeTasksPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">任务中心</text>
<text class="close-btn" @tap="closeTasksPopup">×</text>
</view>
<scroll-view scroll-y class="points-list" @scrolltolower="loadMoreTasks">
<view v-if="tasksLoading && tasksList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!tasksList || tasksList.length === 0" class="status-text">暂无任务</view>
<view v-for="(task, idx) in tasksList" :key="task.id || idx" class="task-item">
<view class="task-left">
<text class="task-name">{{ task.name || '任务' }}</text>
<text class="task-desc">{{ task.description || '' }}</text>
<text class="task-time">{{ formatStamp(task.start_time) }} - {{ formatStamp(task.end_time) }}</text>
<view class="reward-list" v-if="Array.isArray(task.rewards) && task.rewards.length">
<text class="reward-label">奖励</text>
<view class="reward-tags">
<view class="reward-item" v-for="(rw, ri) in task.rewards" :key="ri">
<text class="reward-type">{{ rw.reward_type || '未知' }}</text>
<text class="reward-qty">×{{ rw.quantity || 0 }}</text>
</view>
</view>
</view>
<view class="tier-list" v-if="Array.isArray(task.tiers) && task.tiers.length">
<text class="tier-label">条件</text>
<view class="tier-tags">
<view class="tier-item" v-for="(tr, ti) in task.tiers" :key="ti">
<text class="tier-text">{{ tr.metric || '-' }} {{ tr.operator || '' }} {{ tr.threshold ?? '' }}</text>
</view>
</view>
</view>
</view>
<view class="task-right">
<text class="task-status">{{ formatStatus(task.status) }}</text>
</view>
</view>
<view v-if="tasksLoading && tasksList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!tasksHasMore && tasksList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app' import { onShow, onLoad, onShareAppMessage } from '@dcloudio/uni-app'
import { getUserStats, getPointsBalance } from '../../api/appUser' import { getUserStats, getPointsBalance, getUserPoints, getUserCoupons, redeemCoupon, getItemCards, getTasks } from '../../api/appUser'
const avatar = ref(uni.getStorageSync('avatar') || '') const avatar = ref(uni.getStorageSync('avatar') || '')
const nickname = ref(uni.getStorageSync('nickname') || '') const nickname = ref(uni.getStorageSync('nickname') || '')
@ -76,6 +229,35 @@ const stats = ref(uni.getStorageSync('user_stats') || {})
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
//
const pointsVisible = ref(false)
const pointsList = ref([])
const pointsLoading = ref(false)
const pointsPage = ref(1)
const pointsPageSize = ref(20)
const pointsHasMore = ref(true)
//
const couponsVisible = ref(false)
const couponsTab = ref(0) // 0: 使, 1: 使, 2:
const couponsList = ref([])
const couponsLoading = ref(false)
const couponsPage = ref(1)
const couponsHasMore = ref(true)
const redeemCode = ref('')
//
const itemCardsVisible = ref(false)
const itemCardsList = ref([])
const itemCardsLoading = ref(false)
const tasksVisible = ref(false)
const tasksList = ref([])
const tasksLoading = ref(false)
const tasksPage = ref(1)
const tasksPageSize = ref(20)
const tasksHasMore = ref(true)
async function refresh() { async function refresh() {
const token = uni.getStorageSync('token') const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound') const phoneBound = !!uni.getStorageSync('phone_bound')
@ -127,6 +309,285 @@ function toHelp() {
uni.navigateTo({ url: '/pages/help/index' }) uni.navigateTo({ url: '/pages/help/index' })
} }
//
function showPointsPopup() {
pointsVisible.value = true
//
if (pointsList.value.length === 0) {
pointsPage.value = 1
pointsHasMore.value = true
loadPoints()
}
}
function closePointsPopup() {
pointsVisible.value = false
}
async function loadPoints() {
if (pointsLoading.value) return
pointsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const res = await getUserPoints(user_id, pointsPage.value, pointsPageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) {
list = res.list
total = res.total || 0
} else if (res && Array.isArray(res.data)) {
list = res.data
total = res.total || 0
} else if (Array.isArray(res)) {
list = res
total = res.length
}
if (pointsPage.value === 1) {
pointsList.value = list
} else {
pointsList.value = [...pointsList.value, ...list]
}
if (list.length < pointsPageSize.value || (pointsPage.value * pointsPageSize.value >= total && total > 0)) {
pointsHasMore.value = false
} else {
pointsPage.value += 1
}
if (list.length === 0 && pointsPage.value === 1) {
pointsHasMore.value = false
}
} catch (e) {
console.error('Fetch points error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
pointsLoading.value = false
}
}
function loadMorePoints() {
if (pointsHasMore.value && !pointsLoading.value) {
loadPoints()
}
}
function formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date.getTime())) return dateStr
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${min}`
}
function getActionText(action) {
const map = {
'manual_add': '商品兑换积分',
'manual_sub': '系统扣除',
'register': '注册奖励',
'lottery_cost': '抽奖消耗',
'checkin': '签到奖励'
}
return map[action] || action || '积分变动'
}
//
function showCouponsPopup() {
couponsVisible.value = true
couponsTab.value = 0
redeemCode.value = ''
couponsList.value = []
couponsPage.value = 1
couponsHasMore.value = true
loadCoupons()
}
function closeCouponsPopup() {
couponsVisible.value = false
}
function switchCouponsTab(index) {
if (couponsTab.value === index) return
couponsTab.value = index
if (index !== 2) {
couponsList.value = []
couponsPage.value = 1
couponsHasMore.value = true
loadCoupons()
}
}
async function loadCoupons() {
if (couponsLoading.value) return
couponsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const status = couponsTab.value === 0 ? 0 : 1
const res = await getUserCoupons(user_id, status, couponsPage.value, 20)
let list = []
let total = 0
if (res && Array.isArray(res.list)) {
list = res.list
total = res.total || 0
} else if (res && Array.isArray(res.data)) {
list = res.data
total = res.total || 0
} else if (Array.isArray(res)) {
list = res
total = res.length
}
if (couponsPage.value === 1) {
couponsList.value = list
} else {
couponsList.value = [...couponsList.value, ...list]
}
if (list.length < 20 || (total > 0 && couponsList.value.length >= total)) {
couponsHasMore.value = false
} else {
couponsPage.value += 1
}
if (list.length === 0 && couponsPage.value === 1) {
couponsHasMore.value = false
}
} catch (e) {
console.error('Fetch coupons error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
couponsLoading.value = false
}
}
function loadMoreCoupons() {
if (couponsHasMore.value && !couponsLoading.value && couponsTab.value !== 2) {
loadCoupons()
}
}
async function handleRedeem() {
if (!redeemCode.value) {
uni.showToast({ title: '请输入兑换码', icon: 'none' })
return
}
const user_id = uni.getStorageSync('user_id')
try {
await redeemCoupon(user_id, redeemCode.value)
uni.showToast({ title: '兑换成功', icon: 'success' })
redeemCode.value = ''
refresh() //
} catch (e) {
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
}
}
//
function showItemCardsPopup() {
itemCardsVisible.value = true
loadItemCards()
}
function closeItemCardsPopup() {
itemCardsVisible.value = false
}
async function loadItemCards() {
if (itemCardsLoading.value) return
itemCardsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const res = await getItemCards(user_id)
let list = []
if (res && Array.isArray(res.list)) {
list = res.list
} else if (res && Array.isArray(res.data)) {
list = res.data
} else if (Array.isArray(res)) {
list = res
}
itemCardsList.value = list
} catch (e) {
console.error('Fetch item cards error:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
itemCardsLoading.value = false
}
}
function showTasksPopup() {
tasksVisible.value = true
if (tasksList.value.length === 0) {
tasksPage.value = 1
tasksHasMore.value = true
loadTasks()
}
}
function closeTasksPopup() { tasksVisible.value = false }
async function loadTasks() {
if (tasksLoading.value) return
tasksLoading.value = true
try {
const res = await getTasks(tasksPage.value, tasksPageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
if (tasksPage.value === 1) tasksList.value = list
else tasksList.value = [...tasksList.value, ...list]
if (list.length < tasksPageSize.value || (tasksPage.value * tasksPageSize.value >= total && total > 0)) tasksHasMore.value = false
else tasksPage.value += 1
if (list.length === 0 && tasksPage.value === 1) tasksHasMore.value = false
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
tasksLoading.value = false
}
}
function loadMoreTasks() {
if (tasksHasMore.value && !tasksLoading.value) loadTasks()
}
function formatStamp(ts) {
if (ts === undefined || ts === null || ts === '') return ''
const n = Number(ts)
if (isNaN(n)) return ''
const ms = n > 1e12 ? n : n * 1000
const d = new Date(ms)
if (isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const da = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const min = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${da} ${h}:${min}`
}
function formatStatus(s) {
const n = Number(s)
if (n === 1) return '进行中'
if (n === 2) return '已结束'
if (n === 0) return '未开始'
return '未知'
}
onShow(() => { onShow(() => {
avatar.value = uni.getStorageSync('avatar') || avatar.value avatar.value = uni.getStorageSync('avatar') || avatar.value
nickname.value = uni.getStorageSync('nickname') || nickname.value nickname.value = uni.getStorageSync('nickname') || nickname.value
@ -143,6 +604,14 @@ onShow(() => {
onLoad(() => { onLoad(() => {
refresh() refresh()
}) })
onShareAppMessage(() => {
return {
title: '邀请你一起来加入这个宝藏小程序',
path: `/pages/index/index?invite_code=${inviteCode.value}`,
imageUrl: '/static/logo.png'
}
})
</script> </script>
<style scoped> <style scoped>
@ -156,6 +625,17 @@ onLoad(() => {
.info-item { display: flex; justify-content: space-between; margin-bottom: 12rpx } .info-item { display: flex; justify-content: space-between; margin-bottom: 12rpx }
.info-label { color: #666; font-size: 24rpx } .info-label { color: #666; font-size: 24rpx }
.info-value { font-size: 28rpx } .info-value { font-size: 28rpx }
.invite-value-wrapper { display: flex; align-items: center; }
.share-btn {
margin-left: 10rpx;
font-size: 24rpx;
padding: 0 20rpx;
height: 48rpx;
line-height: 48rpx;
background-color: #007AFF;
color: #fff;
border-radius: 24rpx;
}
.stats { display: flex; background: #fafafa; border-radius: 12rpx; padding: 20rpx; justify-content: space-between; margin-bottom: 20rpx } .stats { display: flex; background: #fafafa; border-radius: 12rpx; padding: 20rpx; justify-content: space-between; margin-bottom: 20rpx }
.stat { flex: 1; align-items: center } .stat { flex: 1; align-items: center }
.stat-label { color: #666; font-size: 24rpx } .stat-label { color: #666; font-size: 24rpx }
@ -171,6 +651,248 @@ onLoad(() => {
.addresses-entry { display: flex; justify-content: space-between; align-items: center; background: #f7f7f7; border-radius: 12rpx; padding: 20rpx } .addresses-entry { display: flex; justify-content: space-between; align-items: center; background: #f7f7f7; border-radius: 12rpx; padding: 20rpx }
.addresses-text { font-size: 28rpx } .addresses-text { font-size: 28rpx }
.addresses-arrow { font-size: 28rpx; color: #999 } .addresses-arrow { font-size: 28rpx; color: #999 }
.refresh { width: 100%; margin-top: 12rpx } .refresh-btn { margin-left: auto; padding: 10rpx }
.refresh-icon { font-size: 40rpx; color: #666; display: block }
.loading .refresh-icon { animation: rotate 1s linear infinite }
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.error { color: #e43; margin-top: 20rpx } .error { color: #e43; margin-top: 20rpx }
</style>
/* 积分弹窗样式 */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 999;
display: flex;
align-items: flex-end;
}
.popup-content {
width: 100%;
height: 70vh;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
display: flex;
flex-direction: column;
}
.popup-header {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #eee;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
}
.close-btn {
font-size: 40rpx;
color: #999;
padding: 0 10rpx;
}
.points-list {
flex: 1;
overflow-y: auto;
padding: 0 30rpx;
box-sizing: border-box; /* 确保 padding 不增加宽度 */
}
.point-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
width: 100%; /* 确保子项占满 */
}
.point-left {
display: flex;
flex-direction: column;
flex: 1; /* 占据剩余空间 */
min-width: 0; /* 关键:允许 flex 子项缩小,防止内容撑开 */
margin-right: 20rpx;
}
.point-desc {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.point-time {
font-size: 24rpx;
color: #999;
}
.point-right {
flex-shrink: 0; /* 防止被压缩 */
margin-left: auto; /* 靠右对齐 */
text-align: right;
max-width: 40%; /* 限制最大宽度 */
}
.point-amount {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
}
.point-amount.positive {
color: #ff4d4f;
}
.point-amount.negative {
color: #52c41a;
}
.status-text {
text-align: center;
color: #999;
margin-top: 60rpx;
font-size: 28rpx;
}
.loading-more, .no-more {
text-align: center;
color: #999;
padding: 20rpx 0;
font-size: 24rpx;
}
/* 优惠券弹窗样式 */
.popup-tabs {
display: flex;
border-bottom: 1rpx solid #eee;
}
.popup-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.popup-tab.active {
color: #007AFF;
font-weight: bold;
}
.popup-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
.redeem-container {
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.redeem-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
margin-bottom: 40rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.redeem-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background-color: #007AFF;
color: #fff;
font-size: 32rpx;
border-radius: 40rpx;
}
.coupon-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
margin: 20rpx 0;
background: #f9f9f9;
border-radius: 12rpx;
border: 1rpx solid #eee;
}
.coupon-left {
display: flex;
flex-direction: column;
flex: 1;
}
.coupon-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.coupon-desc {
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
}
.coupon-time {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.coupon-right {
margin-left: 20rpx;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.coupon-amount-wrapper {
color: #ff4d4f;
margin-bottom: 8rpx;
display: flex;
align-items: baseline;
}
.symbol {
font-size: 24rpx;
font-weight: bold;
}
.amount-value {
font-size: 40rpx;
font-weight: bold;
line-height: 1;
}
.coupon-status {
font-size: 26rpx;
color: #999;
}
.task-item { display: flex; justify-content: space-between; align-items: flex-start; padding: 24rpx; margin: 20rpx 0; background: #f9f9f9; border-radius: 12rpx; border: 1rpx solid #eee }
.task-left { flex: 1; display: flex; flex-direction: column }
.task-name { font-size: 30rpx; font-weight: 700; color: #333 }
.task-desc { margin-top: 6rpx; font-size: 24rpx; color: #666 }
.task-time { margin-top: 6rpx; font-size: 22rpx; color: #999 }
.reward-list { margin-top: 10rpx }
.reward-label, .tier-label { font-size: 24rpx; color: #666 }
.reward-tags, .tier-tags { margin-top: 8rpx; display: flex; flex-wrap: wrap; gap: 8rpx }
.reward-item, .tier-item { background: #fff; border: 1rpx solid #eee; border-radius: 999rpx; padding: 8rpx 12rpx; display: flex; align-items: center; gap: 8rpx }
.reward-type { font-size: 24rpx; color: #333 }
.reward-qty { font-size: 22rpx; color: #999 }
.tier-text { font-size: 24rpx; color: #333 }
.task-right { margin-left: 12rpx; display: flex; align-items: center }
.task-status { font-size: 26rpx; color: #007AFF }
</style>

View File

@ -13,7 +13,7 @@
</view> </view>
<view class="order-right"> <view class="order-right">
<view class="order-amount">{{ formatAmount(item.total_amount || item.amount || item.price) }}</view> <view class="order-amount">{{ formatAmount(item.total_amount || item.amount || item.price) }}</view>
<view class="order-status">{{ statusText(item.status || item.pay_status || item.state) }}</view> <view class="order-status">{{ statusText(item) }}</view>
</view> </view>
</view> </view>
<view v-if="loadingMore" class="loading">加载中...</view> <view v-if="loadingMore" class="loading">加载中...</view>
@ -50,13 +50,18 @@ function formatAmount(a) {
if (a === undefined || a === null) return '' if (a === undefined || a === null) return ''
const n = Number(a) const n = Number(a)
if (Number.isNaN(n)) return String(a) if (Number.isNaN(n)) return String(a)
return `¥${n.toFixed(2)}` const yuan = n / 100
return `¥${yuan.toFixed(2)}`
} }
function statusText(s) { function statusText(item) {
const v = String(s || '').toLowerCase() const v = item && (item.is_draw ?? item.drawed ?? item.completed)
if (v.includes('pend')) return '待付款' const ok = v === true || v === 1 || String(v) === 'true' || String(v) === '1'
if (v.includes('paid') || v.includes('complete') || v.includes('done')) return '已完成' if (ok) return '已完成'
const s = item && (item.status || item.pay_status || item.state)
const t = String(s || '').toLowerCase()
if (t.includes('pend')) return '待付款'
if (t.includes('paid') || t.includes('complete') || t.includes('done')) return '已完成'
return s || '' return s || ''
} }
@ -87,15 +92,20 @@ async function fetchOrders(append) {
}) })
return return
} }
if (append) { if (!append) {
if (currentTab.value === 'completed') {
await fetchAllOrders()
return
} else {
loading.value = true
page.value = 1
hasMore.value = true
orders.value = []
}
} else {
if (!hasMore.value || loadingMore.value) return if (!hasMore.value || loadingMore.value) return
loadingMore.value = true loadingMore.value = true
page.value = page.value + 1 page.value = page.value + 1
} else {
loading.value = true
page.value = 1
hasMore.value = true
orders.value = []
} }
error.value = '' error.value = ''
try { try {
@ -119,6 +129,29 @@ async function fetchOrders(append) {
} }
} }
async function fetchAllOrders() {
const user_id = uni.getStorageSync('user_id')
loading.value = true
page.value = 1
hasMore.value = false
orders.value = []
try {
const first = await getOrders(user_id, apiStatus(), 1, pageSize.value)
const itemsFirst = Array.isArray(first) ? first : (first && first.items) || (first && first.list) || []
const total = (first && first.total) || 0
orders.value = itemsFirst
const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize.value))
for (let p = 2; p <= totalPages; p++) {
const res = await getOrders(user_id, apiStatus(), p, pageSize.value)
const items = Array.isArray(res) ? res : (res && res.items) || (res && res.list) || []
orders.value = orders.value.concat(items)
}
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取订单失败'
} finally {
loading.value = false
}
}
onLoad((opts) => { onLoad((opts) => {
const s = (opts && opts.status) || '' const s = (opts && opts.status) || ''
if (s === 'completed' || s === 'pending') currentTab.value = s if (s === 'completed' || s === 'pending') currentTab.value = s
@ -146,4 +179,4 @@ onReachBottom(() => {
.error { color: #e43; margin-bottom: 12rpx } .error { color: #e43; margin-bottom: 12rpx }
.loading { text-align: center; color: #666; margin: 20rpx 0 } .loading { text-align: center; color: #666; margin: 20rpx 0 }
.end { text-align: center; color: #999; margin: 20rpx 0 } .end { text-align: center; color: #999; margin: 20rpx 0 }
</style> </style>

View File

@ -1,15 +1,15 @@
<template> <template>
<view class="page"> <view class="page">
<view v-if="showNotice" class="notice-mask"> <view v-if="showNotice" class="notice-mask" @touchmove.stop.prevent @tap.stop>
<view class="notice-dialog"> <view class="notice-dialog" @tap.stop>
<view class="notice-title">提示</view> <view class="notice-title">提示</view>
<view class="notice-content">由于价格浮动当前暂不支持自行兑换商品兑换请联系客服核对价格</view> <view class="notice-content">由于价格浮动当前暂不支持自行兑换商品兑换请联系客服核对价格</view>
<view class="notice-actions"> <view class="notice-actions">
<view class="notice-check" @tap="toggleHideForever"> <view class="notice-check" @tap.stop="toggleHideForever">
<view class="check-box" :class="{ on: hideForever }"></view> <view class="check-box" :class="{ on: hideForever }"></view>
<text class="check-text">不再显示</text> <text class="check-text">不再显示</text>
</view> </view>
<button class="notice-btn" type="primary" @tap="onDismissNotice">我知道了</button> <button class="notice-btn" type="primary" @tap.stop="onDismissNotice">我知道了</button>
</view> </view>
</view> </view>
</view> </view>
@ -151,7 +151,7 @@ function normalizeProducts(list) {
id: i.id ?? i.productId ?? i._id ?? i.sku_id ?? String(idx), id: i.id ?? i.productId ?? i._id ?? i.sku_id ?? String(idx),
image: cleanUrl(i.main_image ?? i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''), image: cleanUrl(i.main_image ?? i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
title: i.title ?? i.name ?? i.product_name ?? i.sku_name ?? '', title: i.title ?? i.name ?? i.product_name ?? i.sku_name ?? '',
price: i.price_sale ?? i.price ?? i.price_min ?? i.amount ?? null, price: (() => { const raw = i.price_sale ?? i.price ?? i.price_min ?? i.amount ?? null; return raw == null ? null : (Number(raw) / 100) })(),
points: i.points_required ?? i.points ?? i.integral ?? null, points: i.points_required ?? i.points ?? i.integral ?? null,
stock: i.stock ?? i.inventory ?? i.quantity ?? null, stock: i.stock ?? i.inventory ?? i.quantity ?? null,
link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '') link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '')
@ -159,12 +159,9 @@ function normalizeProducts(list) {
} }
function onProductTap(p) { function onProductTap(p) {
const imgs = (Array.isArray(products.value) ? products.value : []).map(x => x.image).filter(Boolean) const id = p && p.id
const current = p && p.image if (id !== undefined && id !== null && id !== '') {
if (current) { uni.navigateTo({ url: `/pages/shop/detail?id=${id}` })
skipReloadOnce.value = true
try { uni.setStorageSync('shop_skip_reload_once', '1') } catch (_) {}
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
return return
} }
if (p.link && /^\/.+/.test(p.link)) { if (p.link && /^\/.+/.test(p.link)) {