Files
integral-shop/single_uniapp22miao/pages/integral/points.vue
apple 079076a70e miao33: 从 main 同步 single_uniapp22miao,dart-sass 兼容修复,DEPLOY.md 更新
- 从 main 获取 single_uniapp22miao 子项目
- dart-sass: /deep/ -> ::v-deep,calc 运算符加空格
- DEPLOY.md 采用 shccd159 版本(4 子项目架构说明)

Made-with: Cursor
2026-03-16 11:16:42 +08:00

922 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="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()
await 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,
type: 2
})
])
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 => Number(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>