Files
medical-mall/pages/mall/consumer/wallet.uvue

1062 lines
23 KiB
Plaintext
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="wallet-page">
<!-- 顶部栏 -->
<!--<view class="wallet-header">
<text class="back-btn" @click="goBack"></text>
</view>-->
<scroll-view class="wallet-content" scroll-y>
<view class="dashboard-container">
<!-- 左侧/顶部区域:资产信息 -->
<view class="dashboard-main">
<!-- 余额概览 -->
<view class="balance-overview">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
<view class="balance-actions">
<button class="action-btn recharge" @click="recharge">充值</button>
<button class="action-btn withdraw" @click="withdraw">提现</button>
</view>
</view>
<!-- 资产统计 -->
<view class="assets-stats">
<view class="stat-item">
<text class="stat-label">累计充值</text>
<text class="stat-value">¥{{ stats.totalRecharge.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计消费</text>
<text class="stat-value">¥{{ stats.totalConsume.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计提现</text>
<text class="stat-value">¥{{ stats.totalWithdraw.toFixed(2) }}</text>
</view>
</view>
<!-- 快捷功能 -->
<view class="quick-actions">
<view class="action-grid">
<view class="action-item" @click="goToCoupons">
<text class="action-icon">🎫</text>
<text class="action-text">优惠券</text>
</view>
<view class="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
</view>
<view class="action-item" @click="goToPoints">
<text class="action-icon">⭐</text>
<text class="action-text">积分</text>
</view>
<view class="action-item" @click="goToBankCards">
<text class="action-icon">💳</text>
<text class="action-text">银行卡</text>
</view>
</view>
</view>
<!-- 安全提示 (移动端在底部PC端在左侧底部) -->
<view class="security-tips">
<text class="tip-title">安全提示</text>
<text class="tip-item">1. 请妥善保管您的支付密码</text>
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
</view>
</view>
<!-- 右侧/底部区域:交易记录 -->
<view class="dashboard-side">
<!-- 交易记录 -->
<view class="transactions-section">
<view class="section-header">
<text class="section-title">交易记录</text>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@click="changeFilter('expense')">支出</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="transactions.length === 0 && isLoading === false" class="empty-transactions">
<text class="empty-icon">💰</text>
<text class="empty-text">暂无交易记录</text>
<text class="empty-subtext">快去使用钱包功能吧</text>
</view>
<!-- 交易列表 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item">
<view class="transaction-left">
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
<view class="transaction-info">
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
</view>
</view>
<view class="transaction-right">
<text :class="['transaction-amount',
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
</text>
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="hasMore === false && transactions.length > 0" class="no-more">
<text class="no-more-text">没有更多记录了</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 充值弹窗 -->
<view v-if="showRechargePopup" class="recharge-popup">
<view class="popup-mask" @click="closeRechargePopup"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">充值</text>
<text class="popup-close" @click="closeRechargePopup">×</text>
</view>
<view class="popup-body">
<text class="amount-label">充值金额</text>
<view class="amount-input">
<text class="currency-symbol">¥</text>
<input class="amount-field"
v-model="rechargeAmount"
type="number"
placeholder="请输入充值金额"
focus />
</view>
<view class="quick-amounts">
<text v-for="amount in quickAmounts"
:key="amount"
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
@click="selectQuickAmount(amount)">
¥{{ amount }}
</text>
</view>
<text class="recharge-tip">单笔充值最低10元最高5000元</text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
<button class="confirm-btn"
:class="{ disabled: canRecharge === false }"
@click="confirmRecharge">
确认充值
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
type WalletType = {
id: string
user_id: string
balance: number
total_recharge: number
total_consume: number
total_withdraw: number
updated_at: string
}
type TransactionType = {
id: string
user_id: string
change_amount: number
amount: number
current_balance: number
change_type: string
type: string
related_id: string | null
remark: string | null
created_at: string
}
type StatsType = {
totalRecharge: number
totalConsume: number
totalWithdraw: number
}
const balance = ref<number>(0)
const stats = ref<StatsType>({
totalRecharge: 0,
totalConsume: 0,
totalWithdraw: 0
})
const transactions = ref<Array<TransactionType>>([])
const activeFilter = ref<string>('all')
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)
const hasMore = ref<boolean>(true)
const showRechargePopup = ref<boolean>(false)
const rechargeAmount = ref<string>('')
const quickAmounts = [50, 100, 200, 500, 1000]
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
if (userStore == null) return ''
const userInfo = userStore as UTSJSONObject
return userInfo.getString('id') ?? ''
}
// 重置交易记录
const resetTransactions = (): void => {
transactions.value = []
currentPage.value = 1
hasMore.value = true
}
// 加载余额信息
const loadBalance = async (): Promise<void> => {
try {
const realBalance = await supabaseService.getUserBalanceNumber()
balance.value = realBalance
const statsData: StatsType = {
totalRecharge: 0,
totalConsume: 0,
totalWithdraw: 0
} as StatsType
stats.value = statsData
} catch (err) {
console.error('加载钱包异常:', err)
}
}
// 加载交易记录
const loadTransactions = async (loadMore: boolean): Promise<void> => {
if (isLoading.value || (hasMore.value === false && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (userId == '') {
isLoading.value = false
return
}
const page = loadMore ? currentPage.value + 1 : 1
const limit = 20
const data = await supabaseService.getTransactions(page, limit)
const mappedData: Array<TransactionType> = []
for (let i: number = 0; i < data.length; i++) {
const item = data[i]
let id = ''
let amount = 0
let balanceAfter = 0
let type = ''
let remark = ''
let createdAt = ''
if (item instanceof UTSJSONObject) {
id = item.getString('id') ?? ''
amount = item.getNumber('amount') ?? 0
balanceAfter = item.getNumber('balance_after') ?? 0
type = item.getString('type') ?? 'consume'
remark = item.getString('description') ?? ''
createdAt = item.getString('created_at') ?? ''
} else {
const itemObj = item as UTSJSONObject
id = itemObj.getString('id') ?? ''
amount = itemObj.getNumber('amount') ?? 0
balanceAfter = itemObj.getNumber('balance_after') ?? 0
type = itemObj.getString('type') ?? 'consume'
remark = itemObj.getString('description') ?? ''
createdAt = itemObj.getString('created_at') ?? ''
}
const transaction: TransactionType = {
id: id,
user_id: userId,
change_amount: amount,
amount: amount,
current_balance: balanceAfter,
change_type: type,
type: type,
related_id: null,
remark: remark,
created_at: createdAt
} as TransactionType
mappedData.push(transaction)
}
if (loadMore) {
for (let i: number = 0; i < mappedData.length; i++) {
transactions.value.push(mappedData[i])
}
currentPage.value = page
} else {
transactions.value = mappedData
currentPage.value = 1
}
hasMore.value = mappedData.length >= limit
} catch (err) {
console.error('加载交易记录失败:', err)
} finally {
isLoading.value = false
}
}
// 加载钱包数据
const loadWalletData = async (): Promise<void> => {
const userId = getCurrentUserId()
if (userId == '') {
return
}
loadBalance()
loadTransactions(false)
}
// 计算属性
const canRecharge = computed((): boolean => {
const amount = parseFloat(rechargeAmount.value)
if (amount == null || amount < 10 || amount > 5000) {
return false
}
return true
})
// 监听过滤器变化
watch(activeFilter, () => {
resetTransactions()
loadTransactions(false)
})
// 生命周期
onShow(() => {
loadWalletData()
})
// 获取交易图标
const getTransactionIcon = (type: string): string => {
if (type === 'recharge') return '💳'
if (type === 'consume') return '🛒'
if (type === 'withdraw') return '🏦'
if (type === 'refund') return '🔄'
if (type === 'reward') return '🎁'
if (type === 'income') return '💰'
if (type === 'expense') return '📤'
return '💰'
}
// 获取交易标题
const getTransactionTitle = (type: string): string => {
if (type === 'recharge') return '账户充值'
if (type === 'consume') return '商品消费'
if (type === 'withdraw') return '余额提现'
if (type === 'refund') return '订单退款'
if (type === 'reward') return '活动奖励'
if (type === 'income') return '收入'
if (type === 'expense') return '支出'
return '交易'
}
// 格式化时间
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hours}:${minutes}`
}
// 显示更多操作
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['交易记录', '安全设置', '帮助中心'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 交易记录已经在当前页
break
case 1:
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
break
case 2:
uni.navigateTo({
url: '/pages/info/help'
})
break
}
}
})
}
// 充值
const recharge = () => {
showRechargePopup.value = true
rechargeAmount.value = ''
}
// 提现
const withdraw = () => {
uni.navigateTo({
url: '/pages/mall/consumer/withdraw'
})
}
// 跳转到优惠券
const goToCoupons = () => {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
}
// 跳转到红包
const goToRedPackets = () => {
uni.navigateTo({
url: '/pages/mall/consumer/red-packets/index'
})
}
// 跳转到积分
const goToPoints = () => {
// 使用统一的积分页面
uni.navigateTo({
url: '/pages/mall/consumer/points/index'
})
}
// 跳转到银行卡
const goToBankCards = () => {
uni.navigateTo({
url: '/pages/mall/consumer/bank-cards/index'
})
}
// 切换过滤器
const changeFilter = (filter: string) => {
activeFilter.value = filter
}
// 加载更多
const loadMore = () => {
if (hasMore.value && isLoading.value === false) {
loadTransactions(true)
}
}
// 选择快捷金额
const selectQuickAmount = (amount: number): void => {
rechargeAmount.value = amount.toString()
}
// 关闭充值弹窗
const closeRechargePopup = (): void => {
showRechargePopup.value = false
rechargeAmount.value = ''
}
// 确认充值
const confirmRecharge = async (): Promise<void> => {
if (canRecharge.value === false) return
const amount = parseFloat(rechargeAmount.value)
if (amount == null || amount < 10 || amount > 5000) return
uni.showLoading({ title: '处理中...' })
try {
const success = await supabaseService.rechargeBalance(amount)
if (success) {
uni.showToast({
title: '充值成功',
icon: 'success'
})
closeRechargePopup()
loadWalletData()
} else {
uni.showToast({
title: '充值失败',
icon: 'none'
})
}
} catch (e) {
console.error('充值异常:', e)
uni.showToast({
title: '系统异常,请稍后重试',
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
// 返回
const goBack = (): void => {
uni.navigateBack()
}
</script>
<style scoped>
/* 基础样式 */
.wallet-page {
display: flex;
flex-direction: column;
flex: 1; /* Fixed 100vh */
background-color: #f5f5f5;
}
.wallet-content {
flex: 1;
}
.dashboard-container {
display: flex;
flex-direction: column;
padding-bottom: 20px;
}
.dashboard-main {
display: flex;
flex-direction: column;
}
.dashboard-side {
display: flex;
flex-direction: column;
}
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.wallet-content {
padding: 20px;
background-color: #f5f5f5;
}
.dashboard-container {
max-width: 800px;
margin: 0 auto;
width: 100%;
}
.balance-overview, .assets-stats, .quick-actions, .transactions-section, .security-tips {
border-radius: 12px;
}
.popup-content {
width: 400px;
left: 50%;
bottom: 50%;
transform: translate(-50%, 50%);
border-radius: 15px;
}
}
@media screen and (min-width: 1024px) {
.wallet-page {
flex-direction: column; /* 保持纵向,内容区内部处理横向 */
}
.wallet-content {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.dashboard-container {
flex-direction: row; /* 横向排列 */
align-items: flex-start;
/* gap: 20px; REMOVED */
/* max-width: 100%; REMOVED */
}
.dashboard-main {
width: 400px; /* 左侧固定宽度 */
flex-shrink: 0;
margin-right: 20px; /* REPLACED gap */
}
.dashboard-side {
flex: 1; /* 右侧自适应 */
min-width: 0;
}
/* 调整各模块间距 */
.balance-overview,
.assets-stats,
.quick-actions,
.security-tips {
margin-bottom: 20px;
}
.transactions-section {
margin-top: 0; /* 移除顶部间距,与左侧对齐 */
height: 100%;
min-height: 600px; /* 保证右侧高度 */
}
}
/* 模块样式 */
.balance-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
color: #ffffff;
}
.balance-label {
/* display: block; REMOVED */
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
text-align: center;
}
.balance-value {
/* display: block; REMOVED */
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.balance-actions {
display: flex;
flex-direction: row;
/* gap: 20px; REMOVED */
}
.action-btn {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
border: none;
}
.action-btn.recharge {
background-color: #ffffff;
color: #667eea;
margin-right: 20px; /* REPLACED gap */
}
.action-btn.withdraw {
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.assets-stats {
background-color: #ffffff;
padding: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-label {
/* display: block; REMOVED */
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.stat-value {
/* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
color: #333333;
}
.quick-actions {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.action-grid {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.action-icon {
font-size: 28px;
margin-bottom: 8px;
}
.action-text {
font-size: 12px;
color: #666666;
}
.transactions-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.filter-tabs {
/* gap: 15px; REMOVED */
}
.filter-tab {
font-size: 14px;
color: #666666;
padding: 5px 0;
position: relative;
margin-right: 15px; /* REPLACED gap */
border-bottom: 2px solid transparent; /* Prepare for active state */
}
.filter-tab.active {
color: #007aff;
font-weight: bold;
border-bottom: 2px solid #007aff; /* REPLACED ::after */
}
/* ::after removed */
.empty-transactions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
}
.transactions-list {
display: flex;
flex-direction: column;
}
.transaction-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-left {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.transaction-icon {
font-size: 24px;
margin-right: 15px;
}
.transaction-info {
display: flex;
flex-direction: column;
}
.transaction-title {
font-size: 14px;
color: #333333;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
}
.transaction-remark {
font-size: 12px;
color: #666666;
}
.transaction-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.transaction-amount {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-amount.income {
color: #4caf50;
}
.transaction-amount.expense {
color: #333333;
}
.transaction-balance {
font-size: 12px;
color: #999999;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.security-tips {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.tip-title {
/* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
/* display: block; REMOVED */
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.tip-item {
/* display: block; REMOVED */
margin-bottom: 8px;
font-size: 12px;
color: #666666;
line-height: 1.6;
}
.tip-item:last-child {
margin-bottom: 0;
}
.recharge-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
padding: 20px;
}
.popup-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e5e5;
}
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.popup-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.popup-body {
margin-bottom: 20px;
}
.amount-label {
/* display: block; REMOVED */
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.amount-input {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.currency-symbol {
font-size: 20px;
color: #333333;
margin-right: 10px;
}
.amount-field {
flex: 1;
font-size: 24px;
font-weight: bold;
color: #333333;
}
.quick-amounts {
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* gap: 10px; REMOVED */
margin-bottom: 15px;
}
.quick-amount {
padding: 8px 15px;
border: 1px solid #e5e5e5;
border-radius: 15px;
font-size: 14px;
color: #333333;
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.quick-amount.active {
background-color: #007aff;
color: #ffffff;
border-color: #007aff;
}
.recharge-tip {
/* display: block; REMOVED */
font-size: 12px;
color: #999999;
}
.popup-footer {
display: flex;
flex-direction: row;
/* gap: 15px; REMOVED */
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
margin-right: 15px; /* REPLACED gap */
}
.confirm-btn {
background-color: #007aff;
color: #ffffff;
}
.confirm-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
</style>