consumer模块完成度85%,测试连接supabase

This commit is contained in:
2026-01-28 17:28:50 +08:00
parent 66aa909193
commit a4fa00c935
18 changed files with 2108 additions and 1573 deletions

View File

@@ -482,8 +482,18 @@ const goShopping = () => {
const navigateToProduct = (product: any) => {
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
const params = new URLSearchParams()
params.append('id', productId)
params.append('productId', productId)
params.append('price', product.price?.toString() || '0')
// 商品详情页期望的参数名是originalPrice
params.append('originalPrice', (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString())
params.append('name', encodeURIComponent(product.name || ''))
params.append('image', encodeURIComponent(product.image || '/static/product1.jpg'))
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&name=${encodeURIComponent(product.name)}&price=${product.price}&image=${encodeURIComponent(product.image)}`
url: `/pages/mall/consumer/product-detail?${params.toString()}`
})
}

View File

@@ -346,6 +346,7 @@
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(0)
@@ -787,8 +788,20 @@ const addToCart = (product: any) => {
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
// 移除 URLSearchParams 内部的 encodeURIComponent因为 append 会自动编码
// 或者直接构建 URL 字符串以确保兼容性
const name = product.name || ''
const image = product.image || '/static/product1.jpg'
const price = product.price?.toString() || '0'
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
// 手动构建URL避免双重编码问题
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
})
}
const navigateToCategory = (item: any) => {

View File

@@ -156,7 +156,6 @@
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
// // import supa from '@/components/supadb/aksupainstance.uts'
// 响应式数据
const orders = ref<any[]>([])
@@ -365,6 +364,13 @@ const loadOrders = async () => {
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 如果本地存储为空,使用 Mock 数据
if (localOrders.length === 0) {
localOrders = mockOrders
// 可选:将 Mock 数据写入本地存储,以便后续操作生效
// uni.setStorageSync('orders', JSON.stringify(mockOrders))
}
// 过滤当前用户的订单
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)

View File

@@ -153,9 +153,27 @@ export default {
onLoad(options: any) {
const productId = options.productId as string || options.id as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.original_price ? parseFloat(options.original_price) : null
const productName = options.name as string
const productImage = options.image as string
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
// 处理商品名称:如果是编码的则解码,否则直接使用
let productName = options.name as string
if (productName) {
try {
// 尝试解码如果失败不是有效的URI组件则使用原值
productName = decodeURIComponent(productName)
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = options.image as string
if (productImage) {
try {
productImage = decodeURIComponent(productImage)
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId) {
this.loadProductDetail(productId, {
@@ -166,6 +184,13 @@ export default {
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
// 设置导航栏标题为商品名称
if (productName) {
uni.setNavigationBarTitle({
title: productName
})
}
}
},
computed: {
@@ -210,7 +235,6 @@ export default {
if (footprints.length > 50) {
footprints = footprints.slice(0, 50)
}
uni.setStorageSync('footprints', JSON.stringify(footprints))
},

View File

@@ -2,7 +2,7 @@
<template>
<view class="settings-page">
<!-- 顶部栏 -->
<!--<view class="settings-header">
<!--<view class="settings-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">设置</text>
</view>-->
@@ -192,8 +192,20 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onBackPress } from '@dcloudio/uni-app'
// import supa from '@/components/supadb/aksupainstance.uts'
// 拦截返回事件,强制跳转到个人中心页
onBackPress((options) => {
// 无论是什么触发的返回系统返回键或导航栏返回按钮都跳转到profile
// 注意onBackPress 只能在 page 中使用component 中无效
uni.switchTab({
url: '/pages/mall/consumer/profile'
})
// 返回 true 表示阻止默认返回行为
return true
})
type UserType = {
id: string
phone: string | null
@@ -236,8 +248,12 @@ const currentLanguage = ref<string>('简体中文')
const currentTheme = ref<string>('自动')
const appVersion = ref<string>('1.0.0')
const statusBarHeight = ref(0)
// 生命周期
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
loadUserInfo()
loadSettings()
})
@@ -564,11 +580,6 @@ const deleteAccount = () => {
}
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>

View File

@@ -7,113 +7,121 @@
</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 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="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
<!-- 资产统计 -->
<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="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 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 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>
<!-- 安全提示 (移动端在底部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" 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>
<!-- 加载更多 -->
<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>
@@ -251,23 +259,26 @@ const loadBalance = async () => {
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
// 使用本地模拟数据
const mockBalance = {
balance: 12580.00,
total_recharge: 20000.00,
total_consume: 7420.00,
total_withdraw: 0.00
}
// 尝试从本地存储获取
const storedWallet = uni.getStorageSync(`wallet_${userId}`)
const data = storedWallet ? JSON.parse(storedWallet as string) : mockBalance
if (data) {
balance.value = data.balance || 0
// 类型断言,处理 any 类型
const walletData = data as any
balance.value = Number(walletData.balance || 0)
stats.value = {
totalRecharge: data.total_recharge || 0,
totalConsume: data.total_consume || 0,
totalWithdraw: data.total_withdraw || 0
totalRecharge: Number(walletData.total_recharge || 0),
totalConsume: Number(walletData.total_consume || 0),
totalWithdraw: Number(walletData.total_withdraw || 0)
}
}
} catch (err) {
@@ -289,30 +300,49 @@ const loadTransactions = async (loadMore: boolean = false) => {
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('balance_records')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 模拟交易记录数据
const mockTransactions: TransactionType[] = [
{
id: 't1',
user_id: userId,
change_amount: -128.00,
current_balance: 12580.00,
change_type: 'consume',
related_id: 'ord_001',
remark: '购买药品',
created_at: new Date().toISOString()
},
{
id: 't2',
user_id: userId,
change_amount: 500.00,
current_balance: 12708.00,
change_type: 'recharge',
related_id: 'rec_001',
remark: '账户充值',
created_at: new Date(Date.now() - 86400000).toISOString()
},
{
id: 't3',
user_id: userId,
change_amount: -58.50,
current_balance: 12208.00,
change_type: 'consume',
related_id: 'ord_002',
remark: '购买保健品',
created_at: new Date(Date.now() - 172800000).toISOString()
}
]
// 根据过滤器筛选
// 简单模拟分页和筛选
let filtered = mockTransactions
if (activeFilter.value === 'income') {
query = query.gt('change_amount', 0)
filtered = filtered.filter(t => t.change_amount > 0)
} else if (activeFilter.value === 'expense') {
query = query.lt('change_amount', 0)
filtered = filtered.filter(t => t.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 || []
const newTransactions = filtered
if (loadMore) {
transactions.value.push(...newTransactions)
@@ -322,7 +352,8 @@ const loadTransactions = async (loadMore: boolean = false) => {
currentPage.value = 1
}
hasMore.value = newTransactions.length === pageSize.value
// 模拟没有更多数据
hasMore.value = false
} catch (err) {
console.error('加载交易记录异常:', err)
} finally {
@@ -484,6 +515,34 @@ const goBack = () => {
</script>
<style scoped>
/* 基础样式 */
.wallet-page {
display: flex;
flex-direction: column;
height: 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 {
@@ -491,22 +550,16 @@ const goBack = () => {
background-color: #f5f5f5;
}
.balance-overview {
.dashboard-container {
max-width: 800px;
margin: 0 auto;
width: 100%;
}
.balance-overview, .assets-stats, .quick-actions, .transactions-section, .security-tips {
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%;
@@ -518,46 +571,48 @@ const goBack = () => {
@media screen and (min-width: 1024px) {
.wallet-page {
flex-direction: row; /* 大屏下改为横向布局 */
}
.wallet-header {
display: none; /* 大屏下隐藏顶部栏 */
flex-direction: column; /* 保持纵向,内容区内部处理横向 */
}
.wallet-content {
width: 100%;
max-width: 1000px;
max-width: 1200px;
margin: 0 auto;
}
.dashboard-container {
flex-direction: row; /* 横向排列 */
align-items: flex-start;
gap: 20px;
max-width: 100%;
}
.dashboard-main {
width: 400px; /* 左侧固定宽度 */
flex-shrink: 0;
}
.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; /* 保证右侧高度 */
}
}
.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;

View File

@@ -0,0 +1,984 @@
<!-- 钱包页面 -->
<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">单笔充值最低10元最高5000元</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>