Files
integral-shop/single_uniapp22miao/pages/integral/points.vue

921 lines
24 KiB
Vue
Raw Normal View History

<template>
<view class="points-page">
<!-- 白色导航栏 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<text class="iconfont icon-fanhui"></text>
</view>
<text class="nav-title">我的奖金</text>
<view class="nav-right-btn" @click="goToRushBuy">
<text class="rush-btn-text"> 采购 </text>
</view>
</view>
<!-- 奖金概览卡片 -->
<view class="balance-section">
<view class="balance-card">
<text class="balance-label">我的奖金</text>
<text class="balance-total">{{ totalBalance }}</text>
<!-- 奖金和积分卡片 -->
<view class="balance-cards">
<!-- 奖金卡片 -->
<view class="card prize-card">
<text class="card-label">奖金</text>
<text class="card-amount prize-amount">¥{{ prizeAmount }}</text>
<text class="card-tip">可提现</text>
</view>
<!-- 易积分卡片 -->
<view class="card points-card">
<view class="card-top-row">
<text class="card-label">易积分</text>
<view class="exchange-btn" @click="goToShop">
<text class="exchange-text">兑换</text>
</view>
</view>
<text class="card-amount points-amount">{{ pointsInfo.available_points }}</text>
<text class="card-tip">可兑换商品</text>
</view>
</view>
</view>
</view>
<!-- 标签栏 -->
<view class="tabs-section">
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-btn"
:class="{ 'active': currentTab === index }"
@click="switchTab(index)"
>
<text class="tab-text">{{ tab.name }}</text>
</view>
</view>
<!-- 积分明细列表 -->
<scroll-view
class="points-list"
scroll-y
@scrolltolower="loadMore"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<view v-if="pointsList.length > 0" class="list-container">
<view
v-for="item in pointsList"
:key="item.id"
class="point-item"
>
<view class="item-info">
<view class="item-title-row">
<text class="item-title">{{ item.title }}</text>
<text class="item-source" v-if="item.source === 'points'">积分</text>
<!-- <text class="item-source bonus" v-else-if="item.source === 'bonus'">奖金</text> -->
</view>
<text class="item-time">{{ item.created_at }}</text>
</view>
<view class="item-right">
<text
class="item-points"
:class="{ 'income': item.type === 1, 'expense': item.type === 2 }"
>
{{ item.type === 1 ? '+' : '-' }}{{ formatNumber(item.points) }}
</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else-if="!loading" class="empty-state">
<image class="empty-image" src="/static/images/empty.png" mode="aspectFit"></image>
<text class="empty-text">暂无积分记录</text>
</view>
<!-- 加载更多 -->
<view v-if="pointsList.length > 0" class="load-more">
<text v-if="loading" class="load-text">加载中...</text>
<text v-else-if="noMore" class="load-text">没有更多了</text>
</view>
</scroll-view>
</view>
</template>
<script>
import { getWaUserInfo, getWaSelfBonusList, getIntegralList, loginV2, getUserInfo, getIntegralUserByAccount } from '@/api/user.js'
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['isLogin', 'userInfo','uid']),
// 我的奖金 = 奖金(prizeAmount) + 易积分(available_points)
totalBalance() {
const p = Number(this.prizeAmount) || 0
const a = Number(this.pointsInfo.available_points) || 0
return (p + a).toFixed(3)
},
// 获取用户账号
userAccount() {
if (this.userInfo) {
return this.userInfo.account || this.userInfo.phone || this.userInfo.phoneNumber || ''
}
return ''
},
// 获取寄卖商城用户ID优先使用wa_user_id否则使用uid
waUserId() {
// 优先使用明确的wa用户ID字段
if (this.userInfo && (this.userInfo.wa_user_id || this.userInfo.waUserId)) {
return this.userInfo.wa_user_id || this.userInfo.waUserId
}
// 使用store中的uid
if (this.uid) {
return this.uid
}
// 最后使用userInfo中的uid或id
if (this.userInfo) {
return this.userInfo.uid || this.userInfo.id
}
return null
},
// 获取用户手机号用于WA系统关联
userPhone() {
if (this.userInfo) {
return this.userInfo.phone || this.userInfo.mobile || this.userInfo.account
}
return null
}
},
data() {
return {
account: '', // 从URL获取的用户名
password: '123456',
prizeAmount: '0.00', // 可提现奖金money
pointsInfo: {
total_points: 0,
available_points: '0.00', // 易积分score
frozen_points: 0,
today_earn: 0
},
tabs: [
{ name: '收入明细', type: 1 },
{ name: '支出明细', type: 2 }
],
currentTab: 0,
pointsList: [],
page: 1,
limit: 20,
loading: false,
refreshing: false,
noMore: false
}
},
async onLoad(options) {
console.log('onLoad options:', options)
// 获取username优先从options获取否则从window.location获取兼容iframe嵌入场景
let username = (options && options.username) ? options.username : null
// 如果options中没有username尝试从URL中获取兼容H5 iframe嵌入
if (!username) {
username = this.getUrlParam('username')
console.log('从URL获取username:', username)
}
// URL中有username参数时必须调用autoLogin
if (username) {
console.log('获取到username:', username)
this.account = username
// 强制调用autoLogin无论当前是否已登录都要重新登录刷新用户信息
await this.autoLogin()
console.log('autoLogin执行完成isLogin:', this.isLogin)
} else {
// URL没有username时尝试从已登录的用户信息获取账号
if (this.isLogin && this.userInfo && this.userInfo.account) {
this.account = this.userInfo.account
}
// 未登录且有账号则自动登录
if (!this.isLogin && this.account) {
await this.autoLogin()
}
}
this.getUserPoints()
this.loadUserInfo()
this.loadPointsList()
},
onShow() {
// 如果已登录但account为空从userInfo同步
if (this.isLogin && this.userInfo && this.userInfo.account && !this.account) {
this.account = this.userInfo.account
}
// 刷新用户信息和积分明细(需要登录后才能调用)
if (this.isLogin) {
this.getUserPoints()
this.loadUserInfo()
this.loadPointsList()
}
},
methods: {
// 从URL中获取参数兼容多种URL格式
getUrlParam(name) {
// #ifdef H5
try {
// 尝试从当前页面URL获取
let url = window.location.href
// 如果在iframe中尝试获取父页面URL
try {
if (window.parent && window.parent !== window) {
url = window.parent.location.href
}
} catch (e) {
// 跨域情况下无法访问parent.location
console.log('无法访问父页面URL')
}
console.log('解析URL:', url)
// 解析URL参数
const urlObj = new URL(url)
let value = urlObj.searchParams.get(name)
// 如果没找到尝试从hash中获取兼容hash路由
if (!value && url.includes('#')) {
const hashPart = url.split('#')[1]
if (hashPart && hashPart.includes('?')) {
const hashParams = new URLSearchParams(hashPart.split('?')[1])
value = hashParams.get(name)
}
}
return value
} catch (e) {
console.error('解析URL参数失败:', e)
return null
}
// #endif
return null
},
// 自动登录
async autoLogin() {
console.log('autoLogin开始执行account:', this.account)
// 检查账号是否存在
if (!this.account) {
console.warn('autoLogin: 账号为空,跳过登录')
return
}
try {
console.log('正在调用loginV2接口account:', this.account)
const res = await loginV2({
account: this.account,
password: this.password,
spread_spid: this.$Cache.get("spread") || ''
})
console.log("loginV2返回结果:", res)
if (res.data && res.data.token) {
// 保存token到store
this.$store.commit("LOGIN", {
token: res.data.token
})
console.log('token已保存')
// 保存UID
if (res.data.uid) {
this.$store.commit("SETUID", res.data.uid)
console.log('uid已保存:', res.data.uid)
}
// 获取并保存用户信息
console.log('正在获取用户信息...')
const userInfoRes = await getUserInfo()
console.log('getUserInfo返回:', userInfoRes)
if (userInfoRes.data) {
// 更新vuex storestore内部会自动保存到Cache
this.$store.commit("UPDATE_USERINFO", userInfoRes.data)
console.log('用户信息已更新到store:', userInfoRes.data)
// 同步account到本地变量确保后续API调用可用
if (userInfoRes.data.account) {
this.account = userInfoRes.data.account
} else if (userInfoRes.data.phone) {
this.account = userInfoRes.data.phone
}
}
console.log('自动登录成功完成')
} else {
console.warn('loginV2返回数据异常无token:', res)
}
} catch (error) {
console.error('自动登录失败:', error)
uni.showToast({
title: '自动登录失败',
icon: 'none',
duration: 2000
})
} finally {
uni.hideLoading()
}
},
// 返回上一页
goBack() {
uni.navigateBack()
},
// 跳转到抢购页面
goToRushBuy() {
// #ifdef H5
window.location.href = 'https://shop.wenjinhui.com/?#/pages/personal/index'
//window.location.href = 'https://anyue.szxingming.com/?#/pages/personal/index'
// window.location.href = 'https://xiashengjun.com/?#/pages/personal/index'
// window.location.href = 'http://shop.bosenyuan.com/?#/pages/personal/index'
// #endif
// #ifndef H5
uni.navigateTo({
url: '/pages/web-view/index?url=' + encodeURIComponent('https://xiashengjun.com/?#/pages/personal/index')
})
// #endif
},
// 获取用户积分
async getUserPoints() {
// 优先使用登录后的userInfo.account否则使用URL传入的account
const accountToUse = (this.userInfo && this.userInfo.account) || this.userAccount || this.account
if (!accountToUse) {
// 未提供账号时静默返回,不影响其他功能
return
}
try {
const res = await getIntegralUserByAccount(accountToUse)
if (res.data) {
this.pointsInfo.available_points = res.data.integral || 0
}
} catch (error) {
console.error('获取积分失败:', error)
}
},
// 加载寄卖商城用户信息
async loadUserInfo() {
console.log('当前userInfo:', this.userInfo)
console.log('waUserId:', this.waUserId)
console.log('userPhone:', this.userPhone)
// 优先使用waUserId如果没有则使用手机号
const userId = this.waUserId || this.userPhone
if (!userId) {
console.warn('未获取到寄卖商城用户ID或手机号')
return
}
try {
console.log('调用getWaUserInfouserId:', userId)
const res = await getWaUserInfo(userId)
// 接口返回 code: 0 表示成功
if (res.code === 0 && res.data) {
console.log('用户信息接口返回:', res.data)
const data = res.data
// 映射API返回数据到页面展示
// selfBonus -> 我的奖金
// this.totalBalance = this.formatDecimal(data.selfBonus || 0, 3)
// money -> 可提现奖金
this.prizeAmount = this.formatDecimal(data.selfBonus/2 || 0, 2)
}
} catch (error) {
console.error('加载用户信息失败:', error)
}
},
// 切换标签
switchTab(index) {
if (this.currentTab === index) return
this.currentTab = index
this.resetList()
this.loadPointsList()
},
// 重置列表
resetList() {
this.pointsList = []
this.page = 1
this.noMore = false
},
// 加载奖金明细列表
async loadPointsList() {
if (this.loading || this.noMore) return
// 优先使用waUserId如果没有则使用手机号
const userId = this.waUserId || this.userPhone
if (!userId) {
console.warn('未获取到寄卖商城用户ID或手机号')
this.loading = false
return
}
this.loading = true
try {
const type = this.tabs[this.currentTab].type
// 如果是支出明细type === 2同时加载奖金支出和积分支出
if (type === 2) {
// 并行加载奖金明细和积分明细
const [bonusRes, pointsRes] = await Promise.all([
getWaSelfBonusList({
userId: userId,
type: type,
page: this.page,
limit: this.limit
}),
getIntegralList({
page: this.page,
limit: this.limit
})
])
let bonusList = []
let pointsList = []
// 处理奖金明细
if (bonusRes.code === 0 && bonusRes.data) {
bonusList = (bonusRes.data.list || []).map(item => ({
id: `bonus_${item.id}`,
source: 'bonus', // 标记来源:奖金
type: item.type,
points: Math.abs(parseFloat(item.money) || 0),
balance: item.after,
title: item.memo || '奖金支出',
remark: item.memo || '',
created_at: item.createdAt || ''
}))
}
// 处理积分明细(使用 getIntegralList需要过滤出 type === 2 的支出记录)
if (pointsRes.code === 0 && pointsRes.data) {
// 过滤出支出记录type === 2
const expenseRecords = (pointsRes.data.list || []).filter(item => item.type === 2)
pointsList = expenseRecords.map(item => ({
id: `points_${item.id}`,
source: 'points', // 标记来源:积分(与模板中的判断保持一致)
type: item.type || 2, // 积分明细的type2=支出
points: Math.abs(parseFloat(item.integral) || 0), // getIntegralList 返回的是 integral 字段
balance: 0, // getIntegralList 可能没有 balance 字段设为0
title: item.title || '积分支出',
remark: item.title || '',
created_at: item.updateTime || '' // getIntegralList 返回的是 updateTime 字段
}))
}
// 合并两个列表并按时间倒序排序
const mergedList = [...bonusList, ...pointsList].sort((a, b) => {
const timeA = new Date(a.created_at).getTime()
const timeB = new Date(b.created_at).getTime()
return timeB - timeA // 倒序:最新的在前
})
// 如果当前是第一页,直接替换;否则追加
if (this.page === 1) {
this.pointsList = mergedList
} else {
this.pointsList = [...this.pointsList, ...mergedList]
}
// 判断是否还有更多数据(两个接口都返回较少数据时认为没有更多)
const totalCount = bonusList.length + pointsList.length
if (totalCount < this.limit) {
this.noMore = true
}
} else {
// 收入明细只加载奖金明细
const res = await getWaSelfBonusList({
userId: userId,
type: type,
page: this.page,
limit: this.limit
})
// 接口返回 code: 0 表示成功
if (res.code === 0 && res.data) {
console.log('奖金明细接口返回:', res.data)
// 映射API返回数据到页面列表格式
const list = (res.data.list || []).map(item => ({
id: `bonus_${item.id}`,
source: 'bonus',
type: item.type,
points: Math.abs(parseFloat(item.money) || 0),
balance: item.after,
title: item.memo || (item.type === 1 ? '收入' : '支出'),
remark: item.memo || '',
created_at: item.createdAt || ''
}))
this.pointsList = this.page === 1 ? list : [...this.pointsList, ...list]
// 判断是否还有更多数据
if (list.length < this.limit || res.data.page >= res.data.totalPage) {
this.noMore = true
}
}
}
} catch (error) {
console.error('加载明细失败:', error)
uni.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
this.loading = false
this.refreshing = false
}
},
// 下拉刷新
onRefresh() {
this.refreshing = true
this.getUserPoints()
this.loadUserInfo()
this.resetList()
this.loadPointsList()
},
// 加载更多
loadMore() {
if (!this.noMore && !this.loading) {
this.page++
this.loadPointsList()
}
},
// 跳转到积分规则
goToRules() {
uni.navigateTo({
url: '/pages/integral/rules'
})
},
// 跳转到商城
goToShop() {
uni.navigateTo({
url: '/pages/integral/index'
})
},
// 格式化数字(保留指定位小数)
formatNumber(num) {
if (num === null || num === undefined) return '0.000'
return Number(num).toFixed(3)
},
// 格式化小数(指定精度)
formatDecimal(num, precision = 3) {
if (num === null || num === undefined) return '0.' + '0'.repeat(precision)
return Number(num).toFixed(precision)
},
// 获取类型图标
getTypeIcon(type, title) {
if (title.includes('签到')) return '📅'
if (title.includes('购物')) return '🛍️'
if (title.includes('邀请')) return '👥'
if (title.includes('分享')) return '📤'
if (title.includes('兑换')) return '🎁'
if (title.includes('系统')) return '⚙️'
return type === 1 ? '📈' : '📉'
}
}
}
</script>
<style lang="scss" scoped>
.points-page {
min-height: 100vh;
background-color: #F9FAFB;
display: flex;
flex-direction: column;
}
// 白色导航栏
.nav-bar {
background-color: #FFFFFF;
height: 113.344rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 31.996rpx;
border-bottom: 1.344rpx solid rgba(0, 0, 0, 0.1);
}
.nav-back {
width: 72rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 36rpx;
color: #0A0A0A;
}
}
.nav-title {
flex: 1;
font-size: 32rpx;
color: #0A0A0A;
font-weight: normal;
text-align: center;
line-height: 48rpx;
}
.nav-right-btn {
background: linear-gradient(90deg, #FF6900 0%, #F54900 100%);
border-radius: 24rpx;
padding: 8rpx 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.rush-btn-text {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
line-height: 40rpx;
}
// 余额卡片区域
.balance-section {
padding: 30rpx 31rpx 0;
}
.balance-card {
background-color: #FFFFFF;
border: 1rpx solid rgba(0, 0, 0, 0.1);
border-radius: 28rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
gap: 30rpx;
}
.balance-label {
font-size: 32rpx;
color: #4A5565;
display: block;
line-height: 48rpx;
}
.balance-total {
font-size: 60rpx;
color: #0A0A0A;
font-weight: normal;
display: block;
line-height: 72rpx;
}
// 奖金和积分卡片
.balance-cards {
display: flex;
gap: 16rpx;
}
.card {
flex: 1;
border-radius: 20rpx;
padding: 31.996rpx;
min-height: 231.92rpx;
display: flex;
flex-direction: column;
gap: 15.988rpx;
}
.prize-card {
background: linear-gradient(140.01deg, #FFF7ED 0%, #FFEDD4 100%);
}
.points-card {
background: linear-gradient(140.01deg, #EFF6FF 0%, #DBEAFE 100%);
}
.card-top-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.card-label {
font-size: 28rpx;
color: #4A5565;
line-height: 40rpx;
display: block;
}
.card-amount {
font-size: 48rpx;
font-weight: normal;
line-height: 64rpx;
display: block;
}
.prize-amount {
color: #F54900;
}
.points-amount {
color: #155DFC;
}
.card-tip {
font-size: 24rpx;
color: #6A7282;
line-height: 32rpx;
}
.exchange-btn {
background-color: #155DFC;
border-radius: 16rpx;
padding: 0 24rpx;
height: 47.984rpx;
display: flex;
align-items: center;
justify-content: center;
}
.exchange-text {
font-size: 28rpx;
color: #FFFFFF;
font-weight: 500;
line-height: 40rpx;
}
// 标签栏(底部边框设计)
.tabs-section {
padding: 36rpx;
display: flex;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.1);
position: relative;
}
.tab-btn {
flex: 1;
height: 95.99rpx;
line-height: 95.99rpx;
text-align: center;
background-color: transparent;
position: relative;
padding: 9.344rpx 17.344rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
border-bottom: 2rpx solid #F54900;
border-left: 2rpx solid #F54900;
border-right: 2rpx solid #F54900;
border-top: 2rpx solid #F54900;
.tab-text {
color: #F54900;
font-weight: 500;
}
}
}
.tab-text {
font-size: 28rpx;
color: #0A0A0A;
line-height: 40rpx;
transition: all 0.3s;
}
.points-list {
flex: 1;
padding: 1rpx 31.996rpx 0;
}
.list-container {
padding-bottom: 23.992rpx;
display: flex;
flex-direction: column;
gap: 23.992rpx;
}
.point-item {
background-color: #FFFFFF;
border: 1.344rpx solid rgba(0, 0, 0, 0.1);
border-radius: 28rpx;
padding: 33.34rpx 33.34rpx 1.344rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 7.984rpx;
}
.item-title-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.item-title {
font-size: 32rpx;
color: #0A0A0A;
font-weight: normal;
line-height: 48rpx;
}
.item-source {
font-size: 20rpx;
color: #FFFFFF;
background-color: #155DFC;
border-radius: 8rpx;
padding: 4rpx 12rpx;
line-height: 28rpx;
&.bonus {
background-color: #F54900;
}
}
.item-time {
font-size: 28rpx;
color: #6A7282;
line-height: 40rpx;
}
.item-right {
margin-left: 40rpx;
}
.item-points {
font-size: 40rpx;
font-weight: normal;
line-height: 56rpx;
&.income {
color: #00A63E;
}
&.expense {
color: #0A0A0A;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
}
.empty-image {
width: 300rpx;
height: 300rpx;
margin-bottom: 40rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
.load-more {
padding: 47.954rpx 0;
text-align: center;
}
.load-text {
font-size: 32rpx;
color: #99A1AF;
line-height: 48rpx;
}
</style>