Files
medical-mall/doc_mall/consumer/backup_pages/wallett.uvue

986 lines
20 KiB
Plaintext

<!-- 閽卞寘椤甸潰 -->
<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="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>
<!-- 浜ゆ槗璁板綍 -->
<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" 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 && transactions.length > 0" class="no-more">
<text class="no-more-text">娌℃湁鏇村璁板綍浜?/text>
</view>
</view>
<!-- 瀹夊叏鎻愮ず -->
<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>
</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">鍗曠瑪鍏呭€兼渶浣?0鍏冿紝鏈€楂?000鍏?/text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">鍙栨秷</button>
<button class="confirm-btn"
:class="{ disabled: !canRecharge }"
@click="confirmRecharge">
纭鍏呭€?
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, watch } from 'vue'
//import supa from '@/components/supadb/aksupainstance.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
current_balance: number
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
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]
// 璁$畻灞炴€?
const canRecharge = computed(() => {
const amount = parseFloat(rechargeAmount.value)
return !isNaN(amount) && amount >= 10 && amount <= 5000
})
// 鐩戝惉杩囨护鍣ㄥ彉鍖?
watch(activeFilter, () => {
resetTransactions()
loadTransactions()
})
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
loadWalletData()
})
// 閲嶇疆浜ゆ槗璁板綍
const resetTransactions = () => {
transactions.value = []
currentPage.value = 1
hasMore.value = true
}
// 鍔犺浇閽卞寘鏁版嵁
const loadWalletData = async () => {
const userId = getCurrentUserId()
if (!userId) {
uni.navigateTo({
url: '/pages/user/login'
})
return
}
await Promise.all([
loadBalance(),
loadTransactions()
])
}
// 鍔犺浇浣欓淇℃伅
const loadBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
const { data, error } = await supa
.from('user_wallets')
.select('*')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('鍔犺浇閽卞寘澶辫触:', error)
return
}
if (data) {
balance.value = data.balance || 0
stats.value = {
totalRecharge: data.total_recharge || 0,
totalConsume: data.total_consume || 0,
totalWithdraw: data.total_withdraw || 0
}
}
} catch (err) {
console.error('鍔犺浇閽卞寘寮傚父:', err)
}
}
// 鍔犺浇浜ゆ槗璁板綍
const loadTransactions = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (!userId) return
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('balance_records')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 鏍规嵁杩囨护鍣ㄧ瓫閫?
if (activeFilter.value === 'income') {
query = query.gt('change_amount', 0)
} else if (activeFilter.value === 'expense') {
query = query.lt('change_amount', 0)
}
// 鍒嗛〉
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('鍔犺浇浜ゆ槗璁板綍澶辫触:', error)
return
}
const newTransactions = data || []
if (loadMore) {
transactions.value.push(...newTransactions)
currentPage.value = page
} else {
transactions.value = newTransactions
currentPage.value = 1
}
hasMore.value = newTransactions.length === pageSize.value
} catch (err) {
console.error('鍔犺浇浜ゆ槗璁板綍寮傚父:', err)
} finally {
isLoading.value = false
}
}
// 鑾峰彇褰撳墠鐢ㄦ埛ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 鑾峰彇浜ゆ槗鍥炬爣
const getTransactionIcon = (type: string): string => {
const icons: Record<string, string> = {
recharge: '馃挸',
consume: '馃洅',
withdraw: '馃彟',
refund: '馃攧',
reward: '馃巵',
income: '馃挵',
expense: '馃摛'
}
return icons[type] || '馃挵'
}
// 鑾峰彇浜ゆ槗鏍囬
const getTransactionTitle = (type: string): string => {
const titles: Record<string, string> = {
recharge: '璐︽埛鍏呭€?,
consume: '鍟嗗搧娑堣垂',
withdraw: '浣欓鎻愮幇',
refund: '璁㈠崟閫€娆?,
reward: '娲诲姩濂栧姳',
income: '鏀跺叆',
expense: '鏀嚭'
}
return titles[type] || '浜ゆ槗'
}
// 鏍煎紡鍖栨椂闂?
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'
})
}
// 璺宠浆鍒扮Н鍒?
const goToPoints = () => {
uni.navigateTo({
url: '/pages/mall/consumer/points'
})
}
// 璺宠浆鍒伴摱琛屽崱
const goToBankCards = () => {
uni.navigateTo({
url: '/pages/mall/consumer/bank-cards'
})
}
// 鍒囨崲杩囨护鍣?
const changeFilter = (filter: string) => {
activeFilter.value = filter
}
// 鍔犺浇鏇村
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadTransactions(true)
}
}
// 閫夋嫨蹇嵎閲戦
const selectQuickAmount = (amount: number) => {
rechargeAmount.value = amount.toString()
}
// 纭鍏呭€?
const confirmRecharge = async () => {
if (!canRecharge.value) return
const amount = parseFloat(rechargeAmount.value)
if (isNaN(amount)) return
// 杩欓噷搴旇璺宠浆鍒版敮浠橀〉闈㈣繘琛屽厖鍊?
uni.navigateTo({
url: `/pages/mall/consumer/payment?type=recharge&amount=${amount}`
})
closeRechargePopup()
}
// 鍏抽棴鍏呭€煎脊绐?
const closeRechargePopup = () => {
showRechargePopup.value = false
rechargeAmount.value = ''
}
// 杩斿洖
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
/* 鍝嶅簲寮忓竷灞€浼樺寲 */
@media screen and (min-width: 768px) {
.wallet-content {
padding: 20px;
background-color: #f5f5f5;
}
.balance-overview {
border-radius: 12px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.assets-stats, .quick-actions, .transactions-section, .security-tips {
border-radius: 8px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.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: row; /* 澶у睆涓嬫敼涓烘í鍚戝竷灞€ */
}
.wallet-header {
display: none; /* 澶у睆涓嬮殣钘忛《閮ㄦ爮 */
}
.wallet-content {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
}
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.wallet-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
}
.wallet-content {
flex: 1;
}
.balance-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
color: #ffffff;
}
.balance-label {
display: block;
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
text-align: center;
}
.balance-value {
display: block;
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.balance-actions {
display: flex;
gap: 20px;
}
.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;
}
.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;
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;
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.stat-value {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
}
.quick-actions {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.action-grid {
display: flex;
justify-content: space-between;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
}
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.filter-tabs {
display: flex;
gap: 15px;
}
.filter-tab {
font-size: 14px;
color: #666666;
padding: 5px 0;
position: relative;
}
.filter-tab.active {
color: #007aff;
font-weight: bold;
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
.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;
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;
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;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.tip-item {
display: block;
font-size: 12px;
color: #666666;
line-height: 1.6;
margin-bottom: 8px;
}
.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;
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;
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.amount-input {
display: flex;
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-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.quick-amount {
padding: 8px 15px;
border: 1px solid #e5e5e5;
border-radius: 15px;
font-size: 14px;
color: #333333;
}
.quick-amount.active {
background-color: #007aff;
color: #ffffff;
border-color: #007aff;
}
.recharge-tip {
display: block;
font-size: 12px;
color: #999999;
}
.popup-footer {
display: flex;
gap: 15px;
}
.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;
}
.confirm-btn {
background-color: #007aff;
color: #ffffff;
}
.confirm-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
</style>