20260325稳定版
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
<template>
|
||||
<view class="coin-page" :style="{ paddingTop }">
|
||||
<view class="coin-bg"></view>
|
||||
|
||||
<view class="coin-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<u-icon name="arrow-left" size="28" color="#1b1b1b"></u-icon>
|
||||
</view>
|
||||
<text class="header-title">金币中心</text>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
|
||||
<view class="purchase-card">
|
||||
<view class="card-row between">
|
||||
<text class="card-title">金币购买</text>
|
||||
<view class="sign-info" @click="openSignPopup">
|
||||
<u-icon name="calendar" size="24" color="#f37a5b"></u-icon>
|
||||
<text class="sign-text">{{ todaySigned ? '今日已签到' : '签到领金币' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">金币数量</text>
|
||||
<u-number-box
|
||||
v-model="coinAmount"
|
||||
:step="1"
|
||||
:min="1"
|
||||
:integer="true"
|
||||
></u-number-box>
|
||||
</view>
|
||||
<text class="tips">最少购买 1 枚金币,步长 1,可自由增减。</text>
|
||||
<u-button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
:loading="submitting"
|
||||
:disabled="submitting"
|
||||
@click="handlePurchase"
|
||||
>
|
||||
立即购买
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
<view class="record-section">
|
||||
<view class="record-header">
|
||||
<text class="record-title">金币记录</text>
|
||||
<text class="record-count">共 {{ recordList.length }} 条</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="record-empty">正在加载记录...</view>
|
||||
<view v-else-if="!recordList.length" class="record-empty">暂无金币记录</view>
|
||||
|
||||
<view v-else class="record-list">
|
||||
<view
|
||||
v-for="item in recordList"
|
||||
:key="item.id || item.coinLogId || item.createTime"
|
||||
class="record-item"
|
||||
>
|
||||
<view class="record-left">
|
||||
<view class="flex flex-col justify-start items-start">
|
||||
<text class="record-name">{{ item.desc || item.title || '金币变动' }}</text>
|
||||
<template v-if="item.type === '+' && item.desc === '金币购买'">
|
||||
<u-tag type="error" v-if="item.status === 0" shape="circle" size="mini" text="未支付"></u-tag>
|
||||
<u-tag type="success" v-else shape="circle" size="mini" text="已支付"></u-tag>
|
||||
</template>
|
||||
</view>
|
||||
<text class="record-time">
|
||||
{{ formatTime(item.createTime || item.createdAt || item.updateTime) }}
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
class="record-coin"
|
||||
:class="{ positive: item.type === '+' }"
|
||||
>
|
||||
{{ formatCoinChange(item) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<Login ref="loginPopup" :isHide="true" @done="handleLoginSuccess"/>
|
||||
<u-popup :show="signPopupVisible" mode="bottom" round="20" @close="signPopupVisible = false">
|
||||
<view class="sign-popup">
|
||||
<view class="sign-header">
|
||||
<text class="sign-title">连续签到</text>
|
||||
<view class="sign-status" :class="{active: !todaySigned}">
|
||||
{{ todaySigned ? '今日已签到' : '未签到' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="sign-list">
|
||||
<view
|
||||
class="sign-day"
|
||||
v-for="day in signCalendar"
|
||||
:key="day.date"
|
||||
:class="[{signed: day.signed}, {today: day.isToday}]"
|
||||
>
|
||||
<text class="day-text">{{ day.display }}</text>
|
||||
<text class="status-text">{{ day.signed ? '已签' : '未签' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<u-button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
:disabled="todaySigned || signing"
|
||||
:loading="signing"
|
||||
class="sign-btn"
|
||||
@click="handleSign"
|
||||
>
|
||||
{{ todaySigned ? '今日已签到' : '立即签到' }}
|
||||
</u-button>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {coinAddReq, coinListReq, signLogAddReq, signLogListReq} from '@/api/coin'
|
||||
import {dayjs} from '@/uni_modules/iRainna-dayjs/js_sdk/dayjs.min.js'
|
||||
import {getUserInfo} from '@/util/user'
|
||||
import Login from '@/components/Login.vue'
|
||||
|
||||
export default {
|
||||
components: {Login},
|
||||
data() {
|
||||
return {
|
||||
coinAmount: 1,
|
||||
recordList: [],
|
||||
loading: false,
|
||||
submitting: false,
|
||||
paddingTop: '60px',
|
||||
signPopupVisible: false,
|
||||
signLogs: [],
|
||||
todaySigned: false,
|
||||
signing: false,
|
||||
signCalendar: []
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 20
|
||||
this.paddingTop = `${statusBarHeight + 40}px`
|
||||
this.ensureLoginAndLoad()
|
||||
},
|
||||
methods: {
|
||||
ensureLoginAndLoad() {
|
||||
if (!this.ensureLogin()) {
|
||||
this.recordList = []
|
||||
return false
|
||||
}
|
||||
this.fetchRecords()
|
||||
this.fetchSignLogs()
|
||||
return true
|
||||
},
|
||||
ensureLogin() {
|
||||
const {token} = getUserInfo()
|
||||
if (!token) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.loginPopup) this.$refs.loginPopup.open()
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async fetchRecords() {
|
||||
this.loading = true
|
||||
try {
|
||||
const {data} = await coinListReq()
|
||||
if (Array.isArray(data)) {
|
||||
this.recordList = data
|
||||
} else if (Array.isArray(data?.records)) {
|
||||
this.recordList = data.records
|
||||
} else {
|
||||
this.recordList = []
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({title: '加载失败,请稍后重试', icon: 'none'})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async handlePurchase() {
|
||||
if (this.submitting) return
|
||||
if (!this.ensureLogin()) return
|
||||
if (!this.coinAmount || this.coinAmount < 1) {
|
||||
uni.showToast({title: '最少购买 1 枚金币', icon: 'none'})
|
||||
this.coinAmount = 1
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
try {
|
||||
const {data} = await coinAddReq({
|
||||
num: this.coinAmount,
|
||||
type: '+',
|
||||
desc: '金币购买'
|
||||
})
|
||||
const _this = this
|
||||
uni.requestPayment({
|
||||
provider: data.provider,
|
||||
timeStamp: data.timeStamp, // 时间戳
|
||||
nonceStr: data.nonceStr, // 随机字符串
|
||||
package: data.package,
|
||||
signType: data.signType, // 签名算法
|
||||
paySign: data.paySign, // 签名
|
||||
success: function (res) {
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
});
|
||||
_this.fetchRecords()
|
||||
},
|
||||
fail: function (err) {
|
||||
console.log('支付失败', err);
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
const msg = error?.msg || error?.message || '购买失败,请稍后再试'
|
||||
uni.showToast({title: msg, icon: 'none'})
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
formatTime(value) {
|
||||
if (!value) return '--'
|
||||
const time = dayjs(value)
|
||||
if (typeof time.isValid === 'function' && !time.isValid()) return value
|
||||
return time.format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
getCoinValue(item) {
|
||||
const fields = ['coin', 'coinNum', 'number', 'amount', 'value', 'change']
|
||||
for (const field of fields) {
|
||||
if (item[field] !== undefined && item[field] !== null) {
|
||||
const num = Number(item[field])
|
||||
if (!Number.isNaN(num)) return num
|
||||
}
|
||||
}
|
||||
return 0
|
||||
},
|
||||
formatCoinChange(item) {
|
||||
return `${item.type}${item.num}`
|
||||
},
|
||||
handleLoginSuccess() {
|
||||
this.fetchRecords()
|
||||
this.fetchSignLogs()
|
||||
},
|
||||
async fetchSignLogs() {
|
||||
if (!this.ensureLogin()) return
|
||||
try {
|
||||
const {data} = await signLogListReq()
|
||||
const list = Array.isArray(data) ? data : Array.isArray(data?.records) ? data.records : []
|
||||
this.signLogs = list
|
||||
this.todaySigned = list.some(item => dayjs(item.date).isSame(dayjs(), 'day'))
|
||||
this.buildSignCalendar()
|
||||
} catch (error) {
|
||||
this.signLogs = []
|
||||
this.todaySigned = false
|
||||
}
|
||||
},
|
||||
buildSignCalendar() {
|
||||
const today = dayjs()
|
||||
const calendar = []
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = today.subtract(i, 'day')
|
||||
const formatted = date.format('YYYY-MM-DD')
|
||||
const signed = this.signLogs.some(item => dayjs(item.date).isSame(date, 'day'))
|
||||
calendar.push({
|
||||
date: formatted,
|
||||
signed,
|
||||
isToday: date.isSame(today, 'day'),
|
||||
display: date.format('MM/DD')
|
||||
})
|
||||
}
|
||||
this.signCalendar = calendar
|
||||
},
|
||||
openSignPopup() {
|
||||
if (!this.ensureLogin()) return
|
||||
this.signPopupVisible = true
|
||||
if (!this.signCalendar.length) {
|
||||
this.buildSignCalendar()
|
||||
}
|
||||
},
|
||||
async handleSign() {
|
||||
if (this.signing || this.todaySigned) return
|
||||
if (!this.ensureLogin()) return
|
||||
this.signing = true
|
||||
const today = dayjs().format('YYYY-MM-DD')
|
||||
try {
|
||||
await signLogAddReq({date: today})
|
||||
uni.showToast({title: '签到成功', icon: 'success'})
|
||||
this.todaySigned = true
|
||||
this.signLogs.push({date: today})
|
||||
this.buildSignCalendar()
|
||||
setTimeout(() => {
|
||||
this.signPopupVisible = false
|
||||
this.fetchRecords()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
const msg = error?.msg || error?.message || '签到失败'
|
||||
uni.showToast({title: msg, icon: 'none'})
|
||||
} finally {
|
||||
this.signing = false
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.switchTab({url: '/pages/user/index'})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.coin-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding: 40rpx 30rpx 80rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #e1ecff 0%, #f5f0ff 45%, #fef8ff 100%);
|
||||
}
|
||||
|
||||
.coin-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
filter: blur(40rpx);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.coin-header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #1b1b1b;
|
||||
}
|
||||
|
||||
.purchase-card {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
box-shadow: 0 20rpx 50rpx rgba(117, 148, 255, 0.25);
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-row.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sign-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(243, 122, 91, 0.12);
|
||||
color: #f37a5b;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.sign-popup {
|
||||
padding: 40rpx 30rpx 60rpx;
|
||||
}
|
||||
|
||||
.sign-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.sign-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1b1b1b;
|
||||
}
|
||||
|
||||
.sign-status {
|
||||
padding: 12rpx 30rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
background: #f5f5f5;
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
.sign-status.active {
|
||||
background: rgba(52, 199, 89, 0.15);
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.sign-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.sign-day {
|
||||
background: #f7f8ff;
|
||||
border-radius: 18rpx;
|
||||
padding: 20rpx 10rpx;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #5e5f7a;
|
||||
}
|
||||
|
||||
.sign-day.today {
|
||||
border: 2rpx solid #f37a5b;
|
||||
}
|
||||
|
||||
.sign-day.signed {
|
||||
background: rgba(52, 199, 89, 0.15);
|
||||
color: #1f7338;
|
||||
}
|
||||
|
||||
.sign-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1b1b1b;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
margin: 30rpx 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 28rpx;
|
||||
color: #5b5f7b;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 24rpx;
|
||||
color: #8f90aa;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.record-section {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 32rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 20rpx 40rpx rgba(150, 164, 255, 0.2);
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2251;
|
||||
}
|
||||
|
||||
.record-count {
|
||||
font-size: 24rpx;
|
||||
color: #a0a2c2;
|
||||
}
|
||||
|
||||
.record-empty {
|
||||
text-align: center;
|
||||
color: #a0a2c2;
|
||||
font-size: 26rpx;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 26rpx 0;
|
||||
border-bottom: 1px solid rgba(226, 232, 255, 0.7);
|
||||
}
|
||||
|
||||
.record-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.record-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.record-name {
|
||||
font-size: 28rpx;
|
||||
color: #1f2251;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 24rpx;
|
||||
color: #a0a2c2;
|
||||
}
|
||||
|
||||
.record-coin {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #f26c5b;
|
||||
}
|
||||
|
||||
.record-coin.positive {
|
||||
color: #00c78c;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user