继续完善购物逻辑闭环,consumer模块完成度80%

This commit is contained in:
2026-01-27 17:33:39 +08:00
parent f2f208f258
commit 4ab722a118
22 changed files with 5290 additions and 515 deletions

View File

@@ -1,20 +1,32 @@
{
"pages": [
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/index",
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"enablePullDownRefresh": true
}
},
{
"path": "pages/mall/consumer/settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "pages/mall/consumer/wallet",
"style": {
"navigationBarTitleText": "我的钱包"
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "用户登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/category",
"style": {
@@ -150,6 +162,13 @@
"style": {
"navigationBarTitleText": "服务评价"
}
},
{
"path": "pages/mall/consumer/chat",
"style": {
"navigationBarTitleText": "客服聊天",
"navigationStyle": "custom"
}
}
],
"tabBar": {

View File

@@ -16,8 +16,13 @@
</view>
<text class="address-text">{{ getFullAddress(item) }}</text>
</view>
<view class="item-edit" @click.stop="editAddress(item.id)">
<text class="edit-icon">📝</text>
<view class="item-actions">
<view class="action-item" @click.stop="editAddress(item.id)">
<text class="action-icon">📝</text>
</view>
<view class="action-item" @click.stop="deleteAddress(item.id)">
<text class="action-icon"><3E></text>
</view>
</view>
</view>
</view>
@@ -104,6 +109,27 @@ const addAddress = () => {
})
}
// 删除地址
const deleteAddress = (id: string) => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
}
})
}
const editAddress = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/address-edit?id=${id}`
@@ -130,6 +156,26 @@ const selectAddress = (item: Address) => {
</script>
<style>
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.address-list {
max-width: 800px;
margin: 0 auto;
}
.address-list-page {
background-color: #f5f5f5;
}
.footer-btn {
max-width: 800px;
margin: 0 auto;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
}
.address-list-page {
min-height: 100vh;
background-color: #f5f5f5;
@@ -216,12 +262,24 @@ const selectAddress = (item: Address) => {
line-height: 1.4;
}
.item-edit {
.item-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.edit-icon {
.action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.action-icon {
font-size: 20px;
color: #999;
}

View File

@@ -127,6 +127,8 @@
<text class="total-text">合计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<!-- 结算按钮:即使在管理模式下,如果用户想结算也可以(或者在管理模式下隐藏结算,只显示删除) -->
<!-- 需求加购商品前面的按钮时cart购物车页面顶部的管理点击能对加购商品进行批量删除 -->
<button v-if="!isManageMode" class="checkout-btn" @click="goToCheckout">
去结算({{ selectedCount }})
</button>
@@ -154,38 +156,58 @@ const mockRecommendProducts = [
{
id: 'rec_001',
shopId: 'shop_rec_1',
shopName: '极速运动专营店',
shopName: '潮流运动旗舰店',
name: '运动保温杯',
price: 59,
image: 'https://picsum.photos/100/100?random=11',
specification: '黑色 500ml'
specification: '颜色:星空黑 | 容量500ml | 材质304不锈钢',
specDetails: {
color: '星空黑',
capacity: '500ml',
material: '304不锈钢'
}
},
{
id: 'rec_002',
shopId: 'shop_rec_2',
shopName: '品质生活居家',
shopName: '智能家居生活馆',
name: '声波电动牙刷',
price: 129,
image: 'https://picsum.photos/100/100?random=12',
specification: '象牙白 配刷头'
specification: '颜色:珍珠白 | 刷头敏感型×2 | 续航30天',
specDetails: {
color: '珍珠白',
brushHead: '敏感型×2',
batteryLife: '30天'
}
},
{
id: 'rec_003',
shopId: 'shop_rec_3',
shopName: '健康护理大药房',
shopName: '健康防护专家店',
name: '医用护理口罩',
price: 29.9,
image: 'https://picsum.photos/100/100?random=13',
specification: '独立50只'
specification: '规格:三层防护 | 数量50只独立装 | 执行标准YY0469',
specDetails: {
layers: '三层防护',
quantity: '50只',
standard: 'YY0469'
}
},
{
id: 'rec_004',
shopId: 'shop_rec_1',
shopName: '极速运动专营店',
shopId: 'shop_rec_4',
shopName: '户外运动装备店',
name: '专业护膝',
price: 45,
image: 'https://picsum.photos/100/100?random=14',
specification: 'L码 单只装'
specification: '尺码L码 | 材质:记忆棉+弹力布 | 适用:篮球/跑步',
specDetails: {
size: 'L码',
material: '记忆棉+弹力布',
suitableFor: '篮球/跑步'
}
}
]
@@ -442,7 +464,31 @@ const goToCheckout = () => {
})
return
}
uni.navigateTo({ url: '/pages/mall/consumer/checkout' })
// 获取选中的商品
const selectedItems = cartItems.value
.filter(item => item.selected)
.map(item => ({
id: item.id,
product_id: item.id, // 使用商品ID作为product_id
sku_id: item.id, // 使用商品ID作为sku_id
product_name: item.name,
product_image: item.image,
sku_specifications: item.spec,
price: item.price,
quantity: item.quantity
}))
// 跳转到结算页面并传递数据
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
// 通过eventChannel传递数据
res.eventChannel.emit('acceptData', {
selectedItems: selectedItems
})
}
})
}
</script>
@@ -792,17 +838,17 @@ const goToCheckout = () => {
background: #f5f5f5;
}
.recommend-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.recommend-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.recommend-bottom {
display: flex;

View File

@@ -702,7 +702,7 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
const navigateToProduct = (product: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}`
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
})
}

View File

@@ -22,17 +22,26 @@
<!-- 商品列表 -->
<view class="products-section">
<view v-for="item in checkoutItems" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
<!-- 调试信息 -->
<view v-if="checkoutItems.length > 0" class="debug-info">
<text class="debug-text">调试:共{{ checkoutItems.length }}件商品,总价计算:{{ totalAmount }}</text>
</view>
<view v-if="checkoutItems.length > 0">
<view v-for="item in checkoutItems" :key="item.id" class="product-item">
<image class="product-image" :src="item.product_image" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text v-if="item.sku_specifications" class="product-spec">{{ getSpecText(item.sku_specifications) }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ item.price }}</text>
<text class="product-quantity">×{{ item.quantity }}</text>
</view>
</view>
</view>
</view>
<view v-else class="no-products">
<text class="no-products-text">暂无商品信息</text>
</view>
</view>
<!-- 配送方式 -->
@@ -210,11 +219,28 @@
</view>
</view>
</view>
<!-- 确认保存弹窗 -->
<view v-if="showSaveConfirm" class="confirm-popup-mask">
<view class="confirm-popup">
<view class="confirm-header">
<text class="confirm-title">保存地址</text>
</view>
<view class="confirm-content">
<text class="confirm-message">是否保存该地址用于下次使用?</text>
</view>
<view class="confirm-buttons">
<button class="confirm-btn cancel" @click="handleSaveConfirm(false)">仅本次</button>
<button class="confirm-btn confirm" @click="handleSaveConfirm(true)">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch, onUnmounted, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
type CheckoutItemType = {
id: string
@@ -267,10 +293,44 @@ const showNewAddressForm = ref<boolean>(false)
const showSaveConfirm = ref<boolean>(false)
const smartAddressInput = ref<string>('')
// 计算属性
// 计算属性 - 修复价格同步问题
const totalAmount = computed(() => {
return checkoutItems.value.reduce((sum, item) =>
sum + (item.price * item.quantity), 0)
console.log('计算商品总价checkoutItems:', checkoutItems.value)
if (!checkoutItems.value || checkoutItems.value.length === 0) {
console.log('商品列表为空返回0')
return 0
}
// 确保每个商品的价格和数量都是数字类型,并计算总和
const total = checkoutItems.value.reduce((sum, item) => {
// 确保item存在且包含必要的属性
if (!item) return sum
// 将价格转换为数字(确保是数字类型)
let price = 0
if (item.price !== undefined && item.price !== null) {
price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
}
// 将数量转换为数字
let quantity = 0
if (item.quantity !== undefined && item.quantity !== null) {
quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
}
// 验证转换后的数字是否有效
if (isNaN(price) || isNaN(quantity) || price <= 0 || quantity <= 0) {
console.warn('商品价格或数量无效:', item, 'price:', price, 'quantity:', quantity)
return sum
}
const itemTotal = price * quantity
console.log(`商品 ${item.product_name},单价: ${price},数量: ${quantity},小计: ${itemTotal}`)
return sum + itemTotal
}, 0)
console.log('商品总价计算结果:', total)
return total
})
const deliveryFee = computed(() => {
@@ -282,6 +342,7 @@ const discountAmount = computed(() => {
if (!selectedCoupon.value || !selectedCoupon.value.template) return 0
const coupon = selectedCoupon.value.template
// 确保使用计算后的商品总价进行比较
if (totalAmount.value < coupon.min_order_amount) return 0
// 简单处理:假设都是满减券
@@ -289,14 +350,105 @@ const discountAmount = computed(() => {
})
const actualAmount = computed(() => {
let amount = totalAmount.value + deliveryFee.value - discountAmount.value
// 确保所有值都是数字类型
const total = typeof totalAmount.value === 'number' ? totalAmount.value : 0
const delivery = typeof deliveryFee.value === 'number' ? deliveryFee.value : 0
const discount = typeof discountAmount.value === 'number' ? discountAmount.value : 0
// 正确计算:商品总价 + 运费 - 优惠减免
let amount = total + delivery - discount
// 金额必须大于等于0
return amount > 0 ? amount : 0
})
// 监听checkoutItems变化 - 调试用
watch(checkoutItems, (newItems) => {
console.log('checkoutItems变化了:', newItems)
console.log('商品总价计算:', totalAmount.value)
}, { deep: true })
// 页面加载时监听eventChannel
onLoad(() => {
// 优先检查Storage中是否有"立即购买"的数据
const checkoutType = uni.getStorageSync('checkout_type')
if (checkoutType === 'buy_now') {
console.log('检测到立即购买模式从Storage加载数据')
const itemsStr = uni.getStorageSync('checkout_items')
if (itemsStr) {
try {
const items = JSON.parse(itemsStr as string)
console.log('从Storage加载的商品数据:', items)
processCheckoutItems(items)
// 清除Storage避免污染下次进入刷新页面时可能需要保留暂时不清除或者在离开页面时清除
// uni.removeStorageSync('checkout_type')
// uni.removeStorageSync('checkout_items')
loadDefaultAddress()
return // 成功加载,直接返回
} catch (e) {
console.error('解析立即购买数据失败', e)
}
}
}
// 从上一页获取数据
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
console.log('接收到商品数据:', data)
let items = data.selectedItems || []
processCheckoutItems(items)
loadDefaultAddress()
})
} else {
// 如果没有eventChannel尝试从本地存储加载例如从购物车进入
loadFromLocalStorage()
}
})
// 处理商品数据清洗
const processCheckoutItems = (items: any[]) => {
// 数据清洗:确保价格和数量是数字类型
if (items && items.length > 0) {
items = items.map((item: any) => {
// 确保价格是数字
let price = item.price
if (price !== undefined && price !== null) {
price = typeof price === 'string' ? parseFloat(price) : Number(item.price)
if (isNaN(price)) price = 0
} else {
price = 0
}
// 确保数量是数字
let quantity = item.quantity
if (quantity !== undefined && quantity !== null) {
quantity = typeof quantity === 'string' ? parseInt(quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
} else {
quantity = 1
}
return {
...item,
price: Number(price.toFixed(2)), // 保留两位小数
quantity: quantity
}
})
}
checkoutItems.value = items
// 调试:打印每个商品的价格
if (checkoutItems.value && checkoutItems.value.length > 0) {
console.log('清洗后商品价格明细:')
checkoutItems.value.forEach((item: any, index: number) => {
console.log(`商品${index}:`, item.product_name, '价格:', item.price, '类型:', typeof item.price, '数量:', item.quantity)
})
}
}
// 生命周期
onMounted(() => {
loadCheckoutData()
// 监听地址更新事件
uni.$on('addressUpdated', (updatedAddressList: any) => {
addressList.value = updatedAddressList
@@ -316,16 +468,40 @@ onUnmounted(() => {
uni.$off('addressUpdated')
})
// 加载结算数据
const loadCheckoutData = () => {
// 从上一页获取数据
const eventChannel = uni.getEventChannel()
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
checkoutItems.value = data.selectedItems || []
loadDefaultAddress()
})
// 从本地存储加载结算数据(例如从购物车进入)
const loadFromLocalStorage = () => {
// 尝试从本地存储获取购物车选中的商品
const cartData = uni.getStorageSync('cart')
if (cartData) {
try {
const cartItems = JSON.parse(cartData as string) as any[]
// 筛选选中的商品
const selectedCartItems = cartItems.filter(item => item.selected === true)
if (selectedCartItems.length > 0) {
// 转换为CheckoutItemType格式
const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => ({
id: item.id,
product_id: item.productId,
sku_id: item.id, // 购物车中item.id就是SKU ID
product_name: item.name,
product_image: item.image,
sku_specifications: item.spec ? { spec: item.spec } : {},
price: item.price,
quantity: item.quantity
}))
checkoutItems.value = convertedItems
}
} catch (e) {
console.error('解析购物车数据失败:', e)
}
}
loadDefaultAddress()
}
// 加载结算数据兼容旧版本现在主要在onLoad中处理
const loadCheckoutData = () => {
// 为了兼容性保留但主要逻辑已在onLoad中实现
loadFromLocalStorage()
}
// 加载默认地址
@@ -459,7 +635,7 @@ const saveNewAddress = async () => {
}
const userId = getCurrentUserId()
if (!userId) return
// if (!userId) return // 允许未登录用户保存地址用于演示
// 触发保存确认弹窗
showSaveConfirm.value = true
@@ -722,106 +898,52 @@ const submitOrder = async () => {
})
return
}
// 模拟提交成功跳转
// 实际项目中应等待API返回
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=ORDER_MOCK_${Date.now()}&amount=${actualAmount.value}`
})
return;
const userId = getCurrentUserId()
if (!userId) {
uni.showToast({
title: '用户信息错误',
icon: 'none'
})
return
}
// MOCK ORDER SUBMISSION
// 模拟创建成功
try {
const mockOrderId = `order_${Date.now()}`
// 生成订单号
const orderNo = generateOrderNo()
// 创建订单对象
const newOrder = {
id: mockOrderId,
order_no: generateOrderNo(),
user_id: getCurrentUserId() || 'user_001',
merchant_id: checkoutItems.value[0]?.product_id || 'merchant_001', // 简化处理取第一个商品的merchant
status: 1, // 待支付
total_amount: totalAmount.value,
discount_amount: discountAmount.value,
delivery_fee: deliveryFee.value,
actual_amount: actualAmount.value,
payment_method: 0,
payment_status: 0,
delivery_address: selectedAddress.value,
items: checkoutItems.value,
created_at: new Date().toISOString()
}
const orderData = {
order_no: orderNo,
user_id: userId,
merchant_id: 'default', // 这里需要根据商品确定商家
status: 1, // 待支付
total_amount: totalAmount.value,
discount_amount: discountAmount.value,
delivery_fee: deliveryFee.value,
actual_amount: actualAmount.value,
payment_method: 0, // 待选择
payment_status: 0,
delivery_address: selectedAddress.value,
remark: remark.value,
created_at: new Date().toISOString()
}
// 保存到本地存储
const storedOrders = uni.getStorageSync('orders')
let orders: any[] = []
if (storedOrders) {
try {
orders = JSON.parse(storedOrders as string) as any[]
} catch (e) {
console.error('解析订单数据失败', e)
}
}
orders.unshift(newOrder)
uni.setStorageSync('orders', JSON.stringify(orders))
try {
// 创建订单
/* const { data: order, error: orderError } = await supa
.from('orders')
.insert(orderData)
.select()
.single()
if (orderError !== null) {
throw orderError
}
// 创建订单商品项
const orderItems = checkoutItems.value.map(item => ({
order_id: order.id,
product_id: item.product_id,
sku_id: item.sku_id,
product_name: item.product_name,
sku_specifications: item.sku_specifications,
price: item.price,
quantity: item.quantity,
total_amount: item.price * item.quantity
}))
const { error: itemsError } = await supa
.from('order_items')
.insert(orderItems)
if (itemsError !== null) {
throw itemsError
}
// 使用优惠券
if (selectedCoupon.value) {
const { error: couponError } = await supa
.from('user_coupons')
.update({
status: 2, // 已使用
used_at: new Date().toISOString()
})
.eq('id', selectedCoupon.value.id)
if (couponError !== null) {
console.error('更新优惠券状态失败:', couponError)
}
}
// 清空购物车
await clearShoppingCart()
// 跳转到支付页面
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${actualAmount.value}`
}) */
// MOCK ORDER SUBMISSION
// 模拟创建成功
const mockOrderId = `order_mock_${Date.now()}`
uni.showLoading({ title: '提交中...' })
await new Promise(resolve => setTimeout(resolve, 500))
uni.hideLoading()
// 携带价格详情跳转
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}`
url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
})
} catch (err) {
} catch (err) {
console.error('创建订单失败:', err)
uni.showToast({
title: '订单创建失败',
@@ -967,6 +1089,20 @@ const goBack = () => {
padding: 0 15px;
}
.debug-info {
background-color: #f8f9fa;
padding: 10px 15px;
border-bottom: 1px solid #e5e5e5;
margin-bottom: 10px;
}
.debug-text {
font-size: 12px;
color: #666;
display: block;
text-align: center;
}
.product-item {
display: flex;
padding: 15px 0;
@@ -1505,4 +1641,76 @@ const goBack = () => {
font-size: 16px;
border: none;
}
/* 确认弹窗样式 */
.confirm-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10002;
}
.confirm-popup {
background-color: #ffffff;
width: 80%;
max-width: 320px;
border-radius: 12px;
overflow: hidden;
}
.confirm-header {
padding: 20px 0 10px;
text-align: center;
}
.confirm-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.confirm-content {
padding: 0 20px 20px;
text-align: center;
}
.confirm-message {
font-size: 16px;
color: #666666;
}
.confirm-buttons {
display: flex;
border-top: 1px solid #e5e5e5;
}
.confirm-btn {
flex: 1;
height: 50px;
line-height: 50px;
text-align: center;
font-size: 16px;
background-color: #ffffff;
border: none;
border-radius: 0;
}
.confirm-btn::after {
border: none;
}
.confirm-btn.cancel {
color: #666666;
border-right: 1px solid #e5e5e5;
}
.confirm-btn.confirm {
color: #007aff;
font-weight: bold;
}
</style>

View File

@@ -111,9 +111,9 @@ const goShopping = () => {
})
}
const goToDetail = (id: string) => {
const goToDetail = (product: Product) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${id}`
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.original_price || ''}`
})
}

View File

@@ -293,7 +293,7 @@ const viewProduct = (item: FootprintType) => {
if (isEditMode.value) return
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}`
})
}

View File

@@ -788,7 +788,7 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}`
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
})
}
const navigateToCategory = (item: any) => {

View File

@@ -142,82 +142,83 @@ export default {
}
},
onLoad(options: any) {
const orderId = options.orderId as string
const orderId = options.id || options.orderId as string
if (orderId) {
this.loadOrderDetail(orderId)
}
},
methods: {
loadOrderDetail(orderId: string) {
// 模拟加载订单详情数据
this.order = {
id: orderId,
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3, // 1:待支付 2:待发货 3:待收货 4:已完成 5:已取消
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1, // 1:微信支付 2:支付宝 3:余额
payment_status: 1,
delivery_address: {
name: '张三',
phone: '13800138000',
detail: '北京市朝阳区某某街道某某小区1号楼101室'
},
created_at: '2024-01-15 14:30:00'
// 尝试从本地存储加载订单
const ordersStr = uni.getStorageSync('orders')
let localOrder: any = null
if (ordersStr) {
const orders = JSON.parse(ordersStr as string) as any[]
localOrder = orders.find((o: any) => o.id === orderId)
}
this.orderItems = [
{
id: 'item_001',
order_id: orderId,
product_id: 'product_001',
sku_id: 'sku_001',
product_name: '精选好物商品',
sku_specifications: { color: '红色', size: 'M' },
price: 199.99,
quantity: 1,
total_amount: 199.99,
product_image: '/static/product1.jpg'
},
{
id: 'item_002',
order_id: orderId,
product_id: 'product_002',
sku_id: 'sku_002',
product_name: '优质配件',
sku_specifications: { type: '标准版' },
price: 99.99,
quantity: 1,
total_amount: 99.99,
product_image: '/static/product2.jpg'
}
]
if (localOrder) {
// 使用本地存储的数据
this.order = localOrder
this.merchant = {
id: 'merchant_001',
user_id: 'user_001',
shop_name: '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
}
// 处理商品项
if (localOrder.items) {
this.orderItems = localOrder.items.map((item: any) => {
return {
...item,
product_image: item.product_image || item.image || '/static/default-product.png',
product_name: item.product_name || item.name,
sku_specifications: item.sku_specifications || item.specifications
}
})
}
if (this.order.status >= 3) {
this.deliveryInfo = {
courier_name: '李师傅',
courier_phone: '13900139000',
tracking_no: 'YT123456789'
}
// 处理商家信息(模拟,因为本地订单可能没有完整的商家信息)
this.merchant = {
id: localOrder.merchant_id || 'merchant_001',
user_id: 'user_001',
shop_name: localOrder.shopName || '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
}
if (this.order.status >= 3) {
this.deliveryInfo = {
courier_name: '李师傅',
courier_phone: '13900139000',
tracking_no: 'YT123456789'
}
}
} else {
// 回退到模拟数据
this.order = {
id: orderId,
order_no: 'ORD202401150001',
user_id: 'user_001',
merchant_id: 'merchant_001',
status: 3, // 1:待支付 2:待发货 3:待收货 4:已完成 5:已取消
total_amount: 299.98,
discount_amount: 30.00,
delivery_fee: 8.00,
actual_amount: 277.98,
payment_method: 1, // 1:微信支付 2:支付宝 3:余额
payment_status: 1,
delivery_address: {
name: '张三',
phone: '13800138000',
detail: '北京市朝阳区某某街道某某小区1号楼101室'
},
created_at: '2024-01-15 14:30:00'
}
// ... (原有模拟数据逻辑保留)
}
},

View File

@@ -1,13 +1,21 @@
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<view class="orders-header">
<text class="header-title">我的订单</text>
<view class="header-actions">
<text class="search-icon" @click="navigateToSearch">🔍</text>
</view>
<!-- 顶部标题栏 -->
<view class="orders-header">
<view class="header-search full-width">
<input
class="search-input"
type="text"
placeholder="搜索订单号或商品名称"
:value="searchKeyword"
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<text v-if="searchKeyword" class="search-clear" @click="clearSearch">×</text>
<text v-else class="search-icon">🔍</text>
</view>
</view>
<!-- 订单状态筛选 -->
<view class="order-tabs">
@@ -158,6 +166,7 @@ const hasMore = ref<boolean>(true)
const refreshing = ref<boolean>(false)
const page = ref<number>(1)
const activeTab = ref<string>('all')
const searchKeyword = ref<string>('')
// 订单标签页
const orderTabs = reactive([
@@ -350,45 +359,66 @@ const loadOrders = async () => {
}
try {
// Mock Data Loading
await new Promise(resolve => setTimeout(resolve, 500)) // Simulate network delay
// Use filtered mock orders based on user ID logic if needed, but for now just return all mock orders
// In a real app we'd filter by userId
/* const { data, error } = await supa
.from('orders')
.select('*, order_items(*)')
.eq('user_id', userId)
.order('created_at', { ascending: false })
if (error != null) {
console.error('加载订单失败:', error)
return
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
if (data != null) {
orders.value = data.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.status,
create_time: formatDate(order.created_at),
product_amount: order.total_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.order_items as any[]).map((item: any) => ({
id: item.product_id,
name: item.product_name,
price: item.price,
image: '/static/products/1.jpg', // 默认图片
spec: formatSpec(item.sku_specifications),
quantity: item.quantity
}))
}))
} */
// 过滤当前用户的订单
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 暂时显示所有订单用于测试
let userOrders = localOrders
// Using Mock Data
orders.value = mockOrders
// 根据标签页过滤
let filtered = userOrders
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value !== 'all') {
const targetStatus = statusMap[activeTab.value]
filtered = userOrders.filter((o: any) => o.status === targetStatus)
}
// 按时间倒序
filtered.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || a.create_time).getTime()
const timeB = new Date(b.created_at || b.create_time).getTime()
return timeB - timeA
})
// 处理数据格式以适配当前页面
orders.value = filtered.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.status,
create_time: order.created_at || order.create_time,
product_amount: order.total_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.items || order.products || []).map((item: any) => ({
id: item.product_id || item.id,
name: item.product_name || item.name,
price: item.price,
image: item.product_image || item.image || '/static/default-product.png',
spec: item.sku_specifications ? formatSpec(item.sku_specifications) : (item.spec || ''),
quantity: item.quantity
}))
}))
// 更新统计数据
orderTabs[0].count = userOrders.length
orderTabs[1].count = userOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = userOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = userOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = userOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = userOrders.filter((o: any) => o.status === 5).length
} catch (err) {
console.error('加载订单异常:', err)
@@ -403,6 +433,55 @@ const formatDate = (isoString: string): string => {
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 搜索相关函数
const onSearchInput = (e: any) => {
searchKeyword.value = e.detail.value
performSearch()
}
const onSearchConfirm = () => {
performSearch()
}
const clearSearch = () => {
searchKeyword.value = ''
performSearch()
}
const performSearch = () => {
const keyword = searchKeyword.value.trim().toLowerCase()
if (!keyword) {
loadOrders()
return
}
// 在当前订单数据中搜索
const allOrders = getCurrentOrderData() // 这里需要获取完整的订单数据
const filtered = allOrders.filter((order: any) => {
// 搜索订单号
if (order.order_no && order.order_no.toLowerCase().includes(keyword)) {
return true
}
// 搜索商品名称
if (order.products && Array.isArray(order.products)) {
return order.products.some((product: any) => {
return product.name && product.name.toLowerCase().includes(keyword)
})
}
return false
})
orders.value = filtered
}
const getCurrentOrderData = () => {
// 这里应该从本地存储或API获取完整订单数据
// 暂时返回当前orders.value
return orders.value
}
const formatSpec = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
@@ -586,54 +665,95 @@ const goShopping = () => {
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
.header-search.full-width {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.header-actions .search-icon {
.search-input {
flex: 1;
height: 36px;
border: 1px solid #ddd;
border-radius: 18px;
padding: 0 40px 0 16px;
font-size: 14px;
background-color: #f5f5f5;
color: #333;
width: 100%;
}
.search-input::placeholder {
color: #999;
font-size: 12px;
}
.search-input:focus {
outline: none;
border-color: #ff5000;
background-color: white;
}
.search-icon {
position: absolute;
right: 12px;
font-size: 18px;
color: #999;
}
.search-clear {
position: absolute;
right: 12px;
font-size: 20px;
color: #666;
color: #999;
width: 20px;
height: 20px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background-color: #ddd;
cursor: pointer;
}
/* 标签页 */
.order-tabs {
background-color: white;
border-bottom: 1px solid #eee;
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
z-index: 10;
}
.tab-scroll {
white-space: nowrap;
width: 100%;
height: 50px;
white-space: nowrap;
}
.tab-container {
display: flex;
flex-direction: row;
padding: 0 15px;
height: 100%;
padding: 0 10px;
min-width: 100%;
}
.tab-item {
flex-shrink: 0;
padding: 0 15px;
display: flex;
align-items: center;
/* 移除 flex: 1改为自适应宽度或固定最小宽度 */
padding: 15px 15px; /* 增加水平内边距 */
text-align: center;
position: relative;
height: 100%;
margin-right: 10px;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap; /* 防止文字换行 */
flex-shrink: 0; /* 防止被压缩 */
}
.tab-item.active {

View File

@@ -0,0 +1,996 @@
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
<view class="orders-header">
<text class="header-title">我的订单</text>
<view class="header-actions">
<text class="search-icon" @click="navigateToSearch">🔍</text>
</view>
</view>
<!-- 订单状态筛选 -->
<view class="order-tabs">
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
<view class="tab-container">
<view
v-for="tab in orderTabs"
:key="tab.id"
:class="['tab-item', { active: activeTab === tab.id }]"
@click="switchTab(tab.id)"
>
<text class="tab-name">{{ tab.name }}</text>
<text v-if="tab.count > 0" class="tab-count">{{ tab.count }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 订单列表 -->
<scroll-view
scroll-y
class="orders-content"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<!-- 空状态 -->
<view v-if="!loading && orders.length === 0" class="empty-orders">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无订单</text>
<text class="empty-desc">去逛逛,发现心仪的商品</text>
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
</view>
<!-- 订单列表 -->
<view v-else class="order-list">
<view
v-for="order in orders"
:key="order.id"
class="order-card"
>
<!-- 订单头部 -->
<view class="order-header">
<text class="order-no">订单号:{{ order.order_no }}</text>
<text :class="['order-status', getStatusClass(order.status)]">
{{ getStatusText(order.status) }}
</text>
</view>
<!-- 订单商品 -->
<view class="order-products">
<view
v-for="product in order.products"
:key="product.id"
class="order-product"
@click="navigateToProduct(product)"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-spec">{{ product.spec }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-quantity">×{{ product.quantity }}</text>
</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info">
<view class="info-row">
<text class="info-label">商品合计</text>
<text class="info-value">¥{{ order.product_amount }}</text>
</view>
<view class="info-row">
<text class="info-label">运费</text>
<text class="info-value">¥{{ order.shipping_fee }}</text>
</view>
<view class="info-row total">
<text class="info-label">实付款</text>
<text class="info-value total-price">¥{{ order.total_amount }}</text>
</view>
</view>
<!-- 订单操作 -->
<view class="order-actions">
<view v-if="order.status === 1" class="action-buttons">
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
</view>
<view v-if="order.status === 2" class="action-buttons">
<button class="action-btn remind" @click="remindShipping(order.id)">提醒发货</button>
</view>
<view v-if="order.status === 3" class="action-buttons">
<button class="action-btn view" @click="viewLogistics(order.id)">查看物流</button>
<button class="action-btn confirm" @click="confirmReceipt(order.id)">确认收货</button>
</view>
<view v-if="order.status === 4" class="action-buttons">
<button class="action-btn review" @click="goReview(order)">评价</button>
<button class="action-btn repurchase" @click="repurchase(order)">再次购买</button>
</view>
<view v-if="order.status === 5" class="action-buttons">
<button class="action-btn view" @click="viewOrderDetail(order.id)">查看详情</button>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadingMore" class="loading-more">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<view v-if="!hasMore && orders.length > 0" class="no-more">
<text>没有更多订单了</text>
</view>
<!-- 安全区域 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 底部导航 -->
<view class="tabbar-placeholder"></view>
</view>
</template>
<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[]>([])
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const refreshing = ref<boolean>(false)
const page = ref<number>(1)
const activeTab = ref<string>('all')
// 订单标签页
const orderTabs = reactive([
{ id: 'all', name: '全部', count: 12 },
{ id: 'pending', name: '待付款', count: 2 },
{ id: 'shipping', name: '待发货', count: 1 },
{ id: 'delivering', name: '待收货', count: 3 },
{ id: 'completed', name: '已完成', count: 5 },
{ id: 'cancelled', name: '已取消', count: 1 }
])
// Mock 订单数据
const mockOrders = [
{
id: '202311230001',
order_no: '202311230001',
status: 1, // 1:待付款 2:待发货 3:待收货 4:已完成 5:已取消
create_time: '2023-11-23 14:30:22',
product_amount: 378.00,
shipping_fee: 0.00,
total_amount: 378.00,
products: [
{
id: '1001',
name: '无线蓝牙耳机 降噪版',
price: 299.00,
image: 'https://picsum.photos/80/80?random=1',
spec: '白色',
quantity: 1
},
{
id: '1002',
name: '耳机保护套',
price: 29.00,
image: 'https://picsum.photos/80/80?random=2',
spec: '黑色',
quantity: 1
},
{
id: '1003',
name: '数据线',
price: 19.00,
image: 'https://picsum.photos/80/80?random=3',
spec: '1米',
quantity: 2
}
]
},
{
id: '202311220001',
order_no: '202311220001',
status: 2,
create_time: '2023-11-22 10:15:33',
product_amount: 199.00,
shipping_fee: 10.00,
total_amount: 209.00,
products: [
{
id: '2001',
name: '运动T恤 速干面料',
price: 79.00,
image: 'https://picsum.photos/80/80?random=4',
spec: '黑色 L',
quantity: 2
},
{
id: '2002',
name: '运动短裤',
price: 59.00,
image: 'https://picsum.photos/80/80?random=5',
spec: '黑色 M',
quantity: 1
}
]
},
{
id: '202311210001',
order_no: '202311210001',
status: 3,
create_time: '2023-11-21 16:45:12',
product_amount: 299.00,
shipping_fee: 0.00,
total_amount: 299.00,
products: [
{
id: '3001',
name: '智能手环 心率监测',
price: 199.00,
image: 'https://picsum.photos/80/80?random=6',
spec: '黑色',
quantity: 1
},
{
id: '3002',
name: '手环腕带',
price: 29.00,
image: 'https://picsum.photos/80/80?random=7',
spec: '蓝色',
quantity: 2
}
]
},
{
id: '202311200001',
order_no: '202311200001',
status: 4,
create_time: '2023-11-20 09:30:45',
product_amount: 99.00,
shipping_fee: 0.00,
total_amount: 99.00,
products: [
{
id: '4001',
name: '保温杯 500ml',
price: 49.00,
image: 'https://picsum.photos/80/80?random=8',
spec: '白色',
quantity: 2
}
]
},
{
id: '202311190001',
order_no: '202311190001',
status: 5,
create_time: '2023-11-19 14:20:18',
product_amount: 599.00,
shipping_fee: 0.00,
total_amount: 599.00,
products: [
{
id: '5001',
name: '蓝牙音箱 便携式',
price: 199.00,
image: 'https://picsum.photos/80/80?random=9',
spec: '黑色',
quantity: 3
}
]
}
]
// 计算属性:根据当前标签筛选订单
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
const targetStatus = statusMap[activeTab.value]
return orders.value.filter(order => order.status === targetStatus)
})
// 生命周期
onLoad((options) => {
if (options['status']) {
const status = options['status'] as string
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
activeTab.value = status
}
}
if (options['type']) {
const type = options['type'] as string
if (type === 'pending') activeTab.value = 'pending'
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
}
})
onShow(() => {
loadOrders()
})
// 加载订单数据
const loadOrders = async () => {
loading.value = true
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
if (!userId) {
loading.value = false
return
}
try {
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 过滤当前用户的订单
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 暂时显示所有订单用于测试
let userOrders = localOrders
// 根据标签页过滤
let filtered = userOrders
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value !== 'all') {
const targetStatus = statusMap[activeTab.value]
filtered = userOrders.filter((o: any) => o.status === targetStatus)
}
// 按时间倒序
filtered.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || a.create_time).getTime()
const timeB = new Date(b.created_at || b.create_time).getTime()
return timeB - timeA
})
// 处理数据格式以适配当前页面
orders.value = filtered.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.status,
create_time: order.created_at || order.create_time,
product_amount: order.total_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.items || order.products || []).map((item: any) => ({
id: item.product_id || item.id,
name: item.product_name || item.name,
price: item.price,
image: item.product_image || item.image || '/static/default-product.png',
spec: item.sku_specifications ? formatSpec(item.sku_specifications) : (item.spec || ''),
quantity: item.quantity
}))
}))
// 更新统计数据
orderTabs[0].count = userOrders.length
orderTabs[1].count = userOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = userOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = userOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = userOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = userOrders.filter((o: any) => o.status === 5).length
} catch (err) {
console.error('加载订单异常:', err)
} finally {
loading.value = false
}
}
const formatDate = (isoString: string): string => {
if (!isoString) return ''
const date = new Date(isoString)
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
const formatSpec = (specs: any): string => {
if (!specs) return ''
if (typeof specs === 'object') {
return Object.keys(specs).map(key => `${key}:${specs[key]}`).join(' ')
}
return String(specs)
}
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
page.value = 1
orders.value = []
loadOrders()
}
// 获取状态文本
const getStatusText = (status: number): string => {
const statusMap: Record<number, string> = {
1: '待付款',
2: '待发货',
3: '待收货',
4: '已完成',
5: '已取消'
}
return statusMap[status] || '未知状态'
}
// 获取状态类名
const getStatusClass = (status: number): string => {
const classMap: Record<number, string> = {
1: 'status-pending',
2: 'status-shipping',
3: 'status-delivering',
4: 'status-completed',
5: 'status-cancelled'
}
return classMap[status] || 'status-unknown'
}
// 下拉刷新
const onRefresh = () => {
refreshing.value = true
setTimeout(() => {
loadOrders()
refreshing.value = false
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1000)
}
// 上拉加载更多
const loadMore = () => {
if (loadingMore.value || !hasMore.value) return
// 暂未实现分页,直接返回
hasMore.value = false
}
// 订单操作函数
const cancelOrder = (orderId: string) => {
uni.showModal({
title: '确认取消',
content: '确定要取消此订单吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '订单已取消',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 5
orders.value = [...orders.value]
}
}
}
})
}
const payOrder = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${orderId}`
})
}
const remindShipping = (orderId: string) => {
uni.showToast({
title: '已提醒卖家发货',
icon: 'success'
})
}
const viewLogistics = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/logistics?orderId=${orderId}`
})
}
const confirmReceipt = (orderId: string) => {
uni.showModal({
title: '确认收货',
content: '请确认您已收到商品,且商品无误',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '收货成功',
icon: 'success'
})
// 更新订单状态
const index = orders.value.findIndex(order => order.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
}
}
}
})
}
const goReview = (order: any) => {
const productIds = order.products.map((p: any) => p.id).join(',')
uni.navigateTo({
url: `/pages/mall/consumer/review?orderId=${order.id}&productIds=${productIds}`
})
}
const repurchase = (order: any) => {
uni.showModal({
title: '再次购买',
content: '确定要将这些商品加入购物车吗?',
success: (res) => {
if (res.confirm) {
// 这里应该是实际的API调用
uni.showToast({
title: '已加入购物车',
icon: 'success'
})
}
}
})
}
const viewOrderDetail = (orderId: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/order-detail?id=${orderId}`
})
}
// 导航函数
const navigateToSearch = () => {
uni.navigateTo({ url: '/pages/mall/consumer/search' })
}
const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
}
const goShopping = () => {
uni.switchTab({ url: '/pages/mall/consumer/index' })
}
</script>
<style>
.orders-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 头部 */
.orders-header {
background-color: white;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.header-actions .search-icon {
font-size: 20px;
color: #666;
}
/* 标签页 */
.order-tabs {
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: sticky;
top: 50px;
z-index: 10;
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-container {
display: flex;
flex-direction: row;
padding: 0 10px;
min-width: 100%;
}
.tab-item {
/* 移除 flex: 1改为自适应宽度或固定最小宽度 */
padding: 15px 15px; /* 增加水平内边距 */
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap; /* 防止文字换行 */
flex-shrink: 0; /* 防止被压缩 */
}
.tab-item.active {
color: #ff5000;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ff5000;
}
.tab-name {
font-size: 14px;
}
.tab-count {
margin-left: 4px;
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 12px;
text-align: center;
}
/* 内容区 */
.orders-content {
height: calc(100vh - 100px);
}
/* 空状态 */
.empty-orders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: #ddd;
margin-bottom: 20px;
}
.empty-title {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 30px;
}
.go-shopping-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 10px 40px;
font-size: 16px;
}
/* 订单列表 */
.order-list {
padding: 10px;
}
.order-card {
background-color: white;
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 订单头部 */
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 14px;
color: #666;
}
.order-status {
font-size: 14px;
font-weight: bold;
}
.status-pending {
color: #ff5000;
}
.status-shipping {
color: #ff9500;
}
.status-delivering {
color: #007aff;
}
.status-completed {
color: #34c759;
}
.status-cancelled {
color: #999;
}
/* 订单商品 */
.order-products {
padding: 15px;
}
.order-product {
display: flex;
margin-bottom: 15px;
}
.order-product:last-child {
margin-bottom: 0;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 8px;
margin-right: 15px;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 15px;
color: #333;
margin-bottom: 5px;
display: block;
line-height: 1.4;
}
.product-spec {
font-size: 13px;
color: #999;
margin-bottom: 10px;
display: block;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
font-size: 16px;
color: #ff5000;
font-weight: bold;
}
.product-quantity {
font-size: 14px;
color: #666;
}
/* 订单信息 */
.order-info {
padding: 15px;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row.total {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
}
/* 订单操作 */
.order-actions {
padding: 15px;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.action-btn {
padding: 6px 15px;
border-radius: 15px;
font-size: 13px;
border: 1px solid;
background: none;
}
.action-btn.cancel {
color: #666;
border-color: #ccc;
}
.action-btn.pay {
color: #ff5000;
border-color: #ff5000;
}
.action-btn.remind {
color: #666;
border-color: #ccc;
}
.action-btn.view {
color: #666;
border-color: #ccc;
}
.action-btn.confirm {
color: #34c759;
border-color: #34c759;
}
.action-btn.review {
color: #ff9500;
border-color: #ff9500;
}
.action-btn.repurchase {
color: #ff5000;
border-color: #ff5000;
}
/* 加载更多 */
.loading-more {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f5ff;
border-top-color: #ff5000;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-more {
text-align: center;
color: #999;
font-size: 13px;
padding: 20px 0;
}
/* 安全区域 */
.safe-area {
height: 20px;
}
/* 底部导航占位 */
.tabbar-placeholder {
height: 50px;
background-color: #f5f5f5;
}
/* 响应式适配 */
@media screen and (max-width: 320px) {
.tab-item {
padding: 0 10px;
margin-right: 5px;
}
.action-btn {
padding: 6px 10px;
font-size: 12px;
}
}
@media screen and (min-width: 415px) {
.order-card {
margin-bottom: 15px;
}
}
</style>

View File

@@ -2,11 +2,14 @@
<template>
<view class="orders-page">
<!-- 顶部栏 -->
<view class="orders-header">
<view class="header-title">
<text class="title-text">我的订单</text>
<!-- <view class="orders-header" @click="goBackToHome">
<view class="header-left">
<text class="header-title">我的订单</text>
</view>
</view>
<view class="header-right">
<text class="search-icon" @click.stop="navigateToSearch">🔍</text>
</view>
</view> -->
<!-- 订单状态标签页 -->
<view class="order-tabs">
@@ -153,8 +156,9 @@
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import { onShow, onBackPress } from '@dcloudio/uni-app'
import type { OrderType } from '@/types/mall-types.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// import supa from '@/components/supadb/aksupainstance.uts'
type OrderItemType = {
id: string
@@ -203,53 +207,36 @@ onMounted(() => {
// 加载订单数量统计
const loadOrderCounts = async () => {
const userId = getCurrentUserId()
const userId = getCurrentUserId() || 'user_001'
if (!userId) return
try {
// 待支付
const { count: pendingCount, error: pendingError } = await supa
.from('orders')
.select('*', { count: 'exact' })
.eq('user_id', userId)
.eq('status', 1)
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
if (!pendingError) {
tabBadges.value.pending = pendingCount || 0
}
// 过滤当前用户的订单
const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 待支付
const pendingCount = userOrders.filter((o: any) => o.status === 1).length
tabBadges.value.pending = pendingCount
// 待发货
const { count: shippingCount, error: shippingError } = await supa
.from('orders')
.select('*', { count: 'exact' })
.eq('user_id', userId)
.eq('status', 2)
if (!shippingError) {
tabBadges.value.shipping = shippingCount || 0
}
const shippingCount = userOrders.filter((o: any) => o.status === 2).length
tabBadges.value.shipping = shippingCount
// 待收货
const { count: receivingCount, error: receivingError } = await supa
.from('orders')
.select('*', { count: 'exact' })
.eq('user_id', userId)
.eq('status', 3)
if (!receivingError) {
tabBadges.value.receiving = receivingCount || 0
}
const receivingCount = userOrders.filter((o: any) => o.status === 3).length
tabBadges.value.receiving = receivingCount
// 待评价
const { count: reviewCount, error: reviewError } = await supa
.from('orders')
.select('*', { count: 'exact' })
.eq('user_id', userId)
.eq('status', 4)
const reviewCount = userOrders.filter((o: any) => o.status === 4).length
tabBadges.value.review = reviewCount
if (!reviewError) {
tabBadges.value.review = reviewCount || 0
}
} catch (err) {
console.error('加载订单统计异常:', err)
}
@@ -257,16 +244,17 @@ const loadOrderCounts = async () => {
// 加载订单列表
const loadOrders = async (loadMore: boolean = false) => {
const userId = getCurrentUserId()
const userId = getCurrentUserId() || 'user_001' // 默认用户ID
if (!userId) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
uni.navigateTo({
url: '/pages/user/login'
})
return
// uni.showToast({
// title: '请先登录',
// icon: 'none'
// })
// uni.navigateTo({
// url: '/pages/user/login'
// })
// 模拟已登录,用于测试
// return
}
if (isLoading.value || (!hasMore.value && loadMore)) {
@@ -276,55 +264,47 @@ const loadOrders = async (loadMore: boolean = false) => {
isLoading.value = true
try {
const page = loadMore ? currentPage.value + 1 : 1
const page = loadMore ? currentPage.value + 1 : 1
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// 构建查询条件
let query = supa
.from('orders')
.select(`
*,
order_items (
id,
product_id,
product_name,
sku_specifications,
price,
quantity
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 过滤当前用户的订单
const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 根据标签页过滤
let filteredOrders = userOrders
switch (activeTab.value) {
case 'pending':
query = query.eq('status', 1)
filteredOrders = userOrders.filter((o: any) => o.status === 1)
break
case 'shipping':
query = query.eq('status', 2)
filteredOrders = userOrders.filter((o: any) => o.status === 2)
break
case 'receiving':
query = query.eq('status', 3)
filteredOrders = userOrders.filter((o: any) => o.status === 3)
break
case 'review':
query = query.eq('status', 4)
filteredOrders = userOrders.filter((o: any) => o.status === 4)
break
}
// 分页
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
// 按时间倒序
filteredOrders.sort((a: any, b: any) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
})
const { data, error } = await query
if (error !== null) {
console.error('加载订单失败:', error)
return
}
const newOrders = data ?? []
// 分页 (模拟)
const total = filteredOrders.length
const start = (page - 1) * pageSize.value
const end = start + pageSize.value
const pagedOrders = filteredOrders.slice(start, end)
// 处理订单数据,补充商品图片等信息
const processedOrders = await processOrderData(newOrders)
const processedOrders = await processOrderData(pagedOrders)
if (loadMore) {
for (let i = 0; i < processedOrders.length; i++) {
@@ -336,7 +316,7 @@ const loadOrders = async (loadMore: boolean = false) => {
currentPage.value = 1
}
hasMore.value = newOrders.length === pageSize.value
hasMore.value = end < total
} catch (err) {
console.error('加载订单异常:', err)
} finally {
@@ -350,18 +330,26 @@ const processOrderData = async (orders: any[]): Promise<FullOrderType[]> => {
for (let i = 0; i < orders.length; i++) {
const order = orders[i]
const orderItems = order.order_items || []
const orderItems = order.items || [] // 这里的 items 已经在 checkout 存入时有了
// 为每个商品项加载图片
const itemsWithImages = await Promise.all(
orderItems.map(async (item: any) => {
const productImage = await getProductImage(item.product_id)
return {
...item,
product_image: productImage
}
})
)
// const itemsWithImages = await Promise.all(
// orderItems.map(async (item: any) => {
// const productImage = await getProductImage(item.product_id)
// return {
// ...item,
// product_image: productImage
// }
// })
// )
// 本地存储的 items 应该已经有 image 字段了,如果没有,再尝试获取
const itemsWithImages = orderItems.map((item: any) => {
return {
...item,
product_image: item.product_image || item.image || '/static/default-product.png',
product_name: item.product_name || item.name // 兼容不同字段名
}
})
processed.push({
...order,
@@ -450,6 +438,26 @@ const getTotalQuantity = (items: OrderItemType[]): number => {
return items.reduce((total, item) => total + item.quantity, 0)
}
// 返回主页
const goBackToHome = () => {
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
// 导航到搜索页面
const navigateToSearch = () => {
uni.navigateTo({
url: '/pages/mall/consumer/search'
})
}
// 监听手机返回键
onBackPress(() => {
goBackToHome()
return true // 阻止默认返回行为
})
// 标签页切换
const changeTab = (tab: string) => {
activeTab.value = tab
@@ -684,32 +692,49 @@ const goToProfile = () => {
background-color: #ffffff;
padding: 15px;
border-bottom: 1px solid #e5e5e5;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.header-left {
flex: 1;
}
.header-title {
text-align: center;
}
.title-text {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.header-right {
display: flex;
align-items: center;
}
.search-icon {
font-size: 20px;
color: #333333;
padding: 5px;
}
.order-tabs {
background-color: #ffffff;
display: flex;
overflow-x: auto;
white-space: nowrap;
flex-direction: row;
border-bottom: 1px solid #e5e5e5;
justify-content: space-between;
}
.order-tab {
flex: 1;
min-width: 80px;
padding: 15px 10px;
padding: 15px 0;
text-align: center;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.order-tab.active {
@@ -982,30 +1007,44 @@ const goToProfile = () => {
font-size: 14px;
}
/* 底部导航栏 */
.bottom-navigation {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
display: flex;
padding: 10px 0;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #ffffff;
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1px solid #f0f0f0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.nav-item.active {
color: #007aff;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.nav-icon {
font-size: 20px;
margin-bottom: 2px;
font-size: 24px;
margin-bottom: 2px;
}
.nav-text {
font-size: 10px;
font-size: 10px;
color: #999;
}
.nav-item.active .nav-text {
color: #4CAF50;
font-weight: 500;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,26 @@ onMounted(() => {
if (options.orderId) {
orderId.value = options.orderId
orderNo.value = options.orderId // 使用订单ID作为订单号
amount.value = parseFloat(options.amount || 0)
// 优先使用传递的 amount
if (options.amount) {
amount.value = parseFloat(options.amount)
} else {
// 如果没有传 amount尝试从本地存储查找订单
try {
const ordersStr = uni.getStorageSync('orders')
if (ordersStr) {
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId.value)
if (order) {
amount.value = order.actual_amount || order.total_amount || 0
}
}
} catch (e) {
console.error('读取本地订单失败', e)
}
}
// loadOrderInfo() // 暂时注释掉数据库查询
}
})
@@ -59,10 +78,8 @@ onMounted(() => {
// }
const viewOrder = () => {
// 跳转到订单列表或订单详情
// 这里跳转到订单列表页并选中对应tab如果有
uni.redirectTo({
url: '/pages/mall/consumer/orders?status=shipping'
uni.navigateTo({
url: '/pages/mall/consumer/orders'
})
}

View File

@@ -1,17 +1,28 @@
<!-- 支付页面 -->
<template>
<view class="payment-page">
<!-- 顶部栏 -->
<view class="payment-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">收银台</text>
</view>
<view class="payment-content">
<!-- 支付金额 -->
<view class="amount-section">
<text class="amount-label">支付金额</text>
<text class="amount-value">¥{{ amount.toFixed(2) }}</text>
<!-- 价格明细 -->
<view class="price-detail-section">
<text class="section-title">价格明细</text>
<view class="price-detail">
<view class="price-row">
<text class="price-label">商品总价</text>
<text class="price-value">¥{{ productAmount.toFixed(2) }}</text>
</view>
<view class="price-row">
<text class="price-label">运费</text>
<text class="price-value">+¥{{ deliveryFee.toFixed(2) }}</text>
</view>
<view v-if="discountAmount > 0" class="price-row">
<text class="price-label">优惠减免</text>
<text class="price-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
</view>
<view class="price-row total">
<text class="price-label">应付金额</text>
<text class="price-value total-price">¥{{ amount.toFixed(2) }}</text>
</view>
</view>
<text class="order-no">订单号: {{ orderNo }}</text>
</view>
@@ -98,7 +109,8 @@
</template>
<script setup lang="uts">
import { ref, onMounted, watch } from 'vue'
import { ref, onMounted, watch, computed, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
// import { supabase as supa } from '@/components/supadb/aksupainstance.uts'
type PaymentMethodType = {
@@ -119,6 +131,11 @@ const isPaying = ref<boolean>(false)
const showPassword = ref<boolean>(false)
const password = ref<string>('')
// 价格相关变量
const productAmount = ref<number>(0) // 商品总价
const deliveryFee = ref<number>(0) // 运费
const discountAmount = ref<number>(0) // 优惠减免
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
@@ -134,10 +151,64 @@ onMounted(() => {
amount.value = parseFloat(options.amount)
}
// 获取传递的价格详情
if (options.productAmount) {
productAmount.value = parseFloat(options.productAmount)
}
if (options.deliveryFee) {
deliveryFee.value = parseFloat(options.deliveryFee)
}
if (options.discountAmount) {
discountAmount.value = parseFloat(options.discountAmount)
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (!options.productAmount && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 更新本地存储中的订单状态
const updateOrderInStorage = (status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: any[] = []
if (ordersStr) {
orders = JSON.parse(ordersStr as string) as any[]
}
const index = orders.findIndex((o: any) => o.id === orderId.value)
if (index !== -1) {
orders[index].status = status
orders[index].payment_status = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[index].updated_at = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', orderId.value, status)
} else {
console.warn('在Storage (orders)中未找到订单:', orderId.value)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 加载订单信息
const loadOrderInfo = async () => {
try {
@@ -274,6 +345,46 @@ const getPayButtonText = (): string => {
return texts[selectedMethod.value] || '确认支付'
}
// 减少商品库存
const reduceStock = (orderId: string) => {
try {
// 读取订单
const ordersStr = uni.getStorageSync('orders')
if (!ordersStr) return
const orders = JSON.parse(ordersStr as string) as any[]
const order = orders.find((o: any) => o.id === orderId)
if (!order || !order.items) return
// 读取商品库(这里假设商品库也在本地,实际项目中通常在服务器端处理)
// 模拟:如果有本地商品缓存,则更新
/*
const productsStr = uni.getStorageSync('products')
if (productsStr) {
const products = JSON.parse(productsStr as string) as any[]
let hasChange = false
order.items.forEach((item: any) => {
const product = products.find((p: any) => p.id === item.product_id)
if (product && product.stock >= item.quantity) {
product.stock -= item.quantity
hasChange = true
console.log(`商品 ${product.name} 库存减少 ${item.quantity}, 剩余 ${product.stock}`)
}
})
if (hasChange) {
uni.setStorageSync('products', JSON.stringify(products))
}
}
*/
console.log('模拟扣减库存成功', order.items)
} catch (e) {
console.error('扣减库存失败', e)
}
}
// 确认支付
const confirmPayment = async () => {
if (isPaying.value) return
@@ -309,6 +420,11 @@ const confirmPayment = async () => {
await new Promise(resolve => setTimeout(resolve, 2000))
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
// 扣减库存
reduceStock(orderId.value)
/* const { error } = await supa
.from('orders')
.update({
@@ -335,6 +451,9 @@ const confirmPayment = async () => {
duration: 2000
})
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 }) // 2: 待发货
// 跳转到支付成功页面
setTimeout(() => {
uni.redirectTo({
@@ -471,9 +590,91 @@ const forgotPassword = () => {
})
}
// 计算价格明细
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 在组件卸载时移除返回键监听
onUnmounted(() => {
// uni.offBackPress() 在uni-app中不需要手动移除
})
// 返回
const goBack = () => {
uni.navigateBack()
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: async (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
await cancelPayment()
} else {
// 用户选择继续支付,留在当前页面
return
}
}
})
}
// 取消支付,更新订单状态
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
/* const { error } = await supa
.from('orders')
.update({
status: 1, // 待支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
console.error('更新订单状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
return
} */
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
</script>
@@ -511,32 +712,58 @@ const goBack = () => {
overflow-y: auto;
}
.amount-section {
/* 价格明细部分 */
.price-detail-section {
background-color: #ffffff;
padding: 40px 20px;
text-align: center;
padding: 20px 15px;
margin-bottom: 10px;
}
.amount-label {
display: block;
font-size: 14px;
color: #666666;
.price-detail {
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 15px;
}
.amount-value {
display: block;
font-size: 36px;
font-weight: bold;
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.price-row.total {
border-top: 1px solid #e5e5e5;
margin-top: 8px;
padding-top: 15px;
}
.price-label {
font-size: 14px;
color: #666666;
}
.price-value {
font-size: 14px;
color: #333333;
}
.price-value.discount {
color: #4caf50;
}
.price-value.total-price {
font-size: 18px;
color: #ff4757;
margin-bottom: 10px;
font-weight: bold;
}
.order-no {
display: block;
font-size: 12px;
color: #999999;
text-align: center;
}
.methods-section {

View File

@@ -41,6 +41,26 @@
<text class="spec-arrow">></text>
</view>
<!-- 数量选择 -->
<view class="quantity-section">
<text class="quantity-title">数量</text>
<view class="quantity-selector">
<view class="quantity-btn minus" @click="decreaseQuantity">
<text class="quantity-btn-text">-</text>
</view>
<input class="quantity-input"
type="number"
v-model="quantity"
:min="1"
:max="getMaxQuantity()"
@input="validateQuantity" />
<view class="quantity-btn plus" @click="increaseQuantity">
<text class="quantity-btn-text">+</text>
</view>
</view>
<text class="quantity-stock">库存{{ getAvailableStock() }}件</text>
</view>
<!-- 商品详情 -->
<view class="product-description">
<view class="section-title">商品详情</view>
@@ -131,13 +151,32 @@ export default {
}
},
onLoad(options: any) {
const productId = options.productId as string
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
if (productId) {
this.loadProductDetail(productId)
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
name: productName,
image: productImage
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
}
},
computed: {
displayPrice(): number {
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.price
}
return this.product.price
}
},
methods: {
saveFootprint(productId: string) {
const footprintData = uni.getStorageSync('footprints')
@@ -175,39 +214,89 @@ export default {
uni.setStorageSync('footprints', JSON.stringify(footprints))
},
loadProductDetail(productId: string) {
// 模拟加载商品详情数据
loadProductDetail(productId: string, options: any = {}) {
// 根据商品ID生成一个基础价格如果没有传入价格
const generatePriceFromId = (id: string): number => {
// 简单哈希函数将字符串转换为一个在50-500之间的价格
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0 // 转换为32位整数
}
// 将哈希值映射到50-500之间
const price = 50 + Math.abs(hash % 450)
// 保留两位小数
return parseFloat(price.toFixed(2))
}
// 优先使用传入的参数否则根据商品ID生成价格
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
// 根据商品ID生成不同的商品名称使其更真实
const productNames = [
'高品质运动休闲鞋',
'时尚简约双肩背包',
'多功能智能手环',
'便携式蓝牙音箱',
'全自动雨伞',
'抗菌防螨床上四件套',
'不锈钢保温杯',
'无线充电器',
'高清行车记录仪',
'智能体脂秤'
]
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
const productName = options.name ? options.name : productNames[nameIndex]
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_001',
category_id: 'cat_001',
name: '精选好物商品',
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。',
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
'/static/product1.jpg',
'/static/product2.jpg',
'/static/product3.jpg'
],
price: 199.99,
original_price: 299.99,
stock: 100,
sales: 1256,
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: 'merchant_001',
user_id: 'user_001',
shop_name: '优质好店',
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_status: 1,
rating: 4.8,
total_sales: 15680,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
@@ -216,13 +305,15 @@ export default {
loadProductSkus(productId: string) {
// 模拟加载商品SKU数据
const basePrice = this.product.price
this.productSkus = [
{
id: 'sku_001',
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: 199.99,
price: basePrice,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
@@ -232,7 +323,7 @@ export default {
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: 219.99,
price: parseFloat((basePrice * 1.1).toFixed(2)),
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
@@ -328,16 +419,30 @@ export default {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
sku_specifications: sku ? sku.specifications : {},
price: sku ? sku.price : this.product.price,
quantity: this.quantity
}
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
console.log('SKU价格:', sku ? sku.price : '无SKU')
console.log('商品价格:', this.product.price)
console.log('选择的价格:', (sku ? sku.price : this.product.price))
console.log('数量:', this.quantity)
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
sku_specifications: sku ? sku.specifications : {},
price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)),
quantity: Number(this.quantity)
}
// 调试:打印最终传递的数据
console.log('立即购买 - 传递的商品数据:', selectedItem)
// 使用Storage传递数据避免EventChannel可能的问题
uni.setStorageSync('checkout_type', 'buy_now')
uni.setStorageSync('checkout_items', JSON.stringify([selectedItem]))
// 跳转到订单确认页
uni.navigateTo({
@@ -393,6 +498,7 @@ export default {
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 保存原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
@@ -418,6 +524,60 @@ export default {
uni.switchTab({
url: '/pages/mall/consumer/cart'
})
},
// 数量选择相关方法
decreaseQuantity() {
if (this.quantity > 1) {
this.quantity--
}
},
increaseQuantity() {
const maxQuantity = this.getMaxQuantity()
if (this.quantity < maxQuantity) {
this.quantity++
} else {
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
},
validateQuantity() {
// 确保数量是数字
let num = parseInt(this.quantity)
if (isNaN(num)) {
num = 1
}
// 限制在1和最大库存之间
const maxQuantity = this.getMaxQuantity()
if (num < 1) {
num = 1
} else if (num > maxQuantity) {
num = maxQuantity
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
this.quantity = num
},
getMaxQuantity() {
// 如果有选择SKU使用SKU的库存否则使用商品总库存
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.stock
}
return this.product.stock
},
getAvailableStock() {
return this.getMaxQuantity()
}
}
}
@@ -565,6 +725,66 @@ export default {
color: #999;
}
.quantity-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.quantity-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
.quantity-selector {
display: flex;
align-items: center;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
overflow: hidden;
}
.quantity-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.quantity-btn.minus {
border-right: 1rpx solid #e5e5e5;
}
.quantity-btn.plus {
border-left: 1rpx solid #e5e5e5;
}
.quantity-btn-text {
font-size: 28rpx;
color: #333;
}
.quantity-input {
width: 80rpx;
height: 60rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border: none;
background-color: #fff;
}
.quantity-stock {
font-size: 24rpx;
color: #666;
}
.product-description {
background-color: #fff;
padding: 30rpx;

View File

@@ -22,7 +22,7 @@
<view class="nav-stat-item">
<text class="nav-stat-label">余额</text>
<text class="nav-stat-value">¥{{ userStats.balance }}</text>
<text class="nav-stat-value" @click="goToWallet">¥{{ userStats.balance }}</text>
</view>
<view class="nav-stat-item" @click="goToCoupons">
@@ -99,16 +99,16 @@
<text class="tab-text">待支付</text>
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'shipped' }" @click="switchOrderTab('shipped')">
<view class="order-tab" :class="{ active: currentOrderTab === 'toship' }" @click="switchOrderTab('toship')">
<text class="tab-icon">🚚</text>
<text class="tab-text">待发货</text>
<text v-if="orderCounts.toship > 0" class="tab-badge">{{ orderCounts.toship }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'shipped' }" @click="switchOrderTab('shipped')">
<text class="tab-icon">📦</text>
<text class="tab-text">待收货</text>
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
</view>
<view class="order-tab" :class="{ active: currentOrderTab === 'review' }" @click="switchOrderTab('review')">
<text class="tab-icon">⭐</text>
<text class="tab-text">待评价</text>
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
</view>
</view>
</view>
@@ -217,7 +217,7 @@
</view>
<!-- 账户安全 -->
<view class="account-security">
<!-- <view class="account-security">
<view class="section-title">账户安全</view>
<view class="security-items">
<view class="security-item" @click="changePassword">
@@ -242,7 +242,7 @@
</view>
</view>
</view>
</view>
</view> -->
</view>
</template>
@@ -259,8 +259,8 @@ type UserStatsType = {
type OrderCountsType = {
total: number
pending: number
toship: number
shipped: number
review: number
}
type ServiceCountsType = {
@@ -302,9 +302,9 @@ export default {
orderCounts: {
total: 0,
pending: 0,
toship: 0,
shipped: 0,
review: 0
} as OrderCountsType,
} as any,
serviceCounts: {
coupons: 0,
favorites: 0
@@ -332,10 +332,17 @@ export default {
this.initPage()
this.loadUserProfile()
this.loadOrders()
// 监听订单更新事件
uni.$on('orderUpdated', this.handleOrderUpdated)
},
onShow() {
this.refreshData()
},
onUnload() {
// 移除事件监听
uni.$off('orderUpdated', this.handleOrderUpdated)
},
computed: {
// 根据当前Tab筛选订单
filteredOrders(): Array<OrderType> {
@@ -343,8 +350,10 @@ export default {
return this.allOrders
} else if (this.currentOrderTab === 'pending') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 1)
} else if (this.currentOrderTab === 'toship') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 2)
} else if (this.currentOrderTab === 'shipped') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 2 || order.status === 3)
return this.allOrders.filter((order: OrderType): boolean => order.status === 3)
} else if (this.currentOrderTab === 'review') {
return this.allOrders.filter((order: OrderType): boolean => order.status === 4)
}
@@ -355,40 +364,32 @@ export default {
// 加载订单数据
async loadOrders() {
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
if (!userId) return
// const userId = userStore?.id
// if (!userId) return
try {
/* const { data, error } = await supa
.from('orders')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
if (error != null) {
console.error('加载订单失败', error)
return
// 从本地存储加载订单数据
const storedOrders = uni.getStorageSync('orders')
let orders: any[] = []
if (storedOrders) {
orders = JSON.parse(storedOrders as string) as any[]
}
if (data != null) {
this.allOrders = data as any[]
this.recentOrders = this.allOrders
this.allOrders = orders
// 按时间倒序
this.allOrders.sort((a: any, b: any) => {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
})
// 更新角标统计
this.orderCounts = {
total: this.allOrders.length,
pending: this.allOrders.filter((o: any) => o.status === 1).length,
shipped: this.allOrders.filter((o: any) => o.status === 2 || o.status === 3).length,
review: this.allOrders.filter((o: any) => o.status === 4).length
}
} */
// 过滤最近的订单
this.recentOrders = this.allOrders.slice(0, 5)
// MOCK ORDERS
this.allOrders = this.recentOrders
// 更新角标统计 (确保状态码一致: 1=待支付, 2=待发货, 3=待收货, 4=待评价)
this.orderCounts = {
total: this.allOrders.length,
pending: this.allOrders.filter((o: any) => o.status === 1).length,
shipped: this.allOrders.filter((o: any) => o.status === 2 || o.status === 3).length,
toship: this.allOrders.filter((o: any) => o.status === 2).length, // 修复仅计算状态2为待发货
shipped: this.allOrders.filter((o: any) => o.status === 3).length, // 修复仅计算状态3为待收货
review: this.allOrders.filter((o: any) => o.status === 4).length
}
} catch (e) {
@@ -581,12 +582,20 @@ export default {
})
},
// 跳转设置
goToSettings() {
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
},
// 跳转钱包
goToWallet() {
uni.navigateTo({
url: '/pages/mall/consumer/wallet'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/consumer/orders?type=${type}`
@@ -696,6 +705,26 @@ export default {
uni.navigateTo({
url: '/pages/mall/consumer/bind-email'
})
},
// 处理订单更新事件
handleOrderUpdated(data: any) {
// 当收到订单更新事件时,刷新订单数据
console.log('收到订单更新事件:', data)
this.refreshData()
// 显示提示
if (data.status === 1) {
uni.showToast({
title: '订单已保存到待支付',
icon: 'success'
})
} else if (data.status === 2) {
uni.showToast({
title: '支付成功,订单待发货',
icon: 'success'
})
}
}
}
}

View File

@@ -537,7 +537,7 @@ const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.originalPrice || ''}`
})
}

View File

@@ -0,0 +1,702 @@
<!-- 设置页面 -->
<template>
<view class="settings-page">
<!-- 顶部栏 -->
<view class="settings-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">设置</text>
</view>
<scroll-view class="settings-content" scroll-y>
<!-- 账户设置 -->
<view class="settings-section">
<text class="section-title">账户设置</text>
<view class="section-list">
<view class="list-item" @click="goToProfile">
<text class="item-icon">👤</text>
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="goToAddress">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="changePassword">
<text class="item-icon">🔒</text>
<text class="item-text">修改密码</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="bindPhone">
<text class="item-icon">📱</text>
<text class="item-text">手机绑定</text>
<view class="item-right">
<text class="item-status" :class="{ bound: userInfo.phone }">
{{ userInfo.phone ? '已绑定' : '未绑定' }}
</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="bindEmail">
<text class="item-icon">📧</text>
<text class="item-text">邮箱绑定</text>
<view class="item-right">
<text class="item-status" :class="{ bound: userInfo.email }">
{{ userInfo.email ? '已绑定' : '未绑定' }}
</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 消息通知 -->
<view class="settings-section">
<text class="section-title">消息通知</text>
<view class="section-list">
<view class="list-item">
<text class="item-icon">🔔</text>
<text class="item-text">订单消息</text>
<switch :checked="notifications.order" @change="toggleNotification('order')" />
</view>
<view class="list-item">
<text class="item-icon">🎁</text>
<text class="item-text">促销活动</text>
<switch :checked="notifications.promotion" @change="toggleNotification('promotion')" />
</view>
<view class="list-item">
<text class="item-icon">⭐</text>
<text class="item-text">评价提醒</text>
<switch :checked="notifications.review" @change="toggleNotification('review')" />
</view>
</view>
</view>
<!-- 隐私设置 -->
<view class="settings-section">
<text class="section-title">隐私设置</text>
<view class="section-list">
<view class="list-item">
<text class="item-icon">👁️</text>
<text class="item-text">隐藏购物记录</text>
<switch :checked="privacy.hidePurchase" @change="togglePrivacy('hidePurchase')" />
</view>
<view class="list-item">
<text class="item-icon">🔍</text>
<text class="item-text">允许通过手机号找到我</text>
<switch :checked="privacy.allowSearchByPhone" @change="togglePrivacy('allowSearchByPhone')" />
</view>
<view class="list-item">
<text class="item-icon">💬</text>
<text class="item-text">接收商家消息</text>
<switch :checked="privacy.receiveMerchantMsg" @change="togglePrivacy('receiveMerchantMsg')" />
</view>
</view>
</view>
<!-- 通用设置 -->
<view class="settings-section">
<text class="section-title">通用设置</text>
<view class="section-list">
<view class="list-item" @click="clearCache">
<text class="item-icon">🗑️</text>
<text class="item-text">清除缓存</text>
<view class="item-right">
<text class="item-cache">{{ cacheSize }}</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="changeLanguage">
<text class="item-icon">🌐</text>
<text class="item-text">语言设置</text>
<view class="item-right">
<text class="item-status">{{ currentLanguage }}</text>
<text class="item-arrow"></text>
</view>
</view>
<view class="list-item" @click="changeTheme">
<text class="item-icon">🎨</text>
<text class="item-text">主题设置</text>
<view class="item-right">
<text class="item-status">{{ currentTheme }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 关于我们 -->
<view class="settings-section">
<text class="section-title">关于我们</text>
<view class="section-list">
<view class="list-item" @click="aboutUs">
<text class="item-icon"></text>
<text class="item-text">关于商城</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="userAgreement">
<text class="item-icon">📜</text>
<text class="item-text">用户协议</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="privacyPolicy">
<text class="item-icon">🛡️</text>
<text class="item-text">隐私政策</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="checkUpdate">
<text class="item-icon">🔄</text>
<text class="item-text">检查更新</text>
<view class="item-right">
<text class="item-status">{{ appVersion }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 客服与反馈 -->
<view class="settings-section">
<text class="section-title">客服与反馈</text>
<view class="section-list">
<view class="list-item" @click="contactService">
<text class="item-icon">💬</text>
<text class="item-text">联系客服</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="feedback">
<text class="item-icon">📝</text>
<text class="item-text">意见反馈</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="rateApp">
<text class="item-icon">⭐</text>
<text class="item-text">给个好评</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
<!-- 账号注销 -->
<view class="delete-account-section">
<text class="delete-account" @click="deleteAccount">注销账号</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
// import supa from '@/components/supadb/aksupainstance.uts'
type UserType = {
id: string
phone: string | null
email: string | null
nickname: string | null
avatar_url: string | null
}
type NotificationType = {
order: boolean
promotion: boolean
review: boolean
}
type PrivacyType = {
hidePurchase: boolean
allowSearchByPhone: boolean
receiveMerchantMsg: boolean
}
const userInfo = ref<UserType>({
id: '',
phone: null,
email: null,
nickname: null,
avatar_url: null
})
const notifications = ref<NotificationType>({
order: true,
promotion: true,
review: true
})
const privacy = ref<PrivacyType>({
hidePurchase: false,
allowSearchByPhone: true,
receiveMerchantMsg: true
})
const cacheSize = ref<string>('0.0 MB')
const currentLanguage = ref<string>('简体中文')
const currentTheme = ref<string>('自动')
const appVersion = ref<string>('1.0.0')
// 生命周期
onMounted(() => {
loadUserInfo()
loadSettings()
})
// 加载用户信息
const loadUserInfo = () => {
const userStore = uni.getStorageSync('userInfo')
if (userStore) {
userInfo.value = userStore
}
}
// 加载设置
const loadSettings = () => {
// 从本地存储加载设置
const savedNotifications = uni.getStorageSync('userNotifications')
if (savedNotifications) {
notifications.value = savedNotifications
}
const savedPrivacy = uni.getStorageSync('userPrivacy')
if (savedPrivacy) {
privacy.value = savedPrivacy
}
// 计算缓存大小
calculateCacheSize()
// 获取应用版本
// @ts-ignore
const appInfo = uni.getAppBaseInfo()
if (appInfo?.appVersion) {
appVersion.value = appInfo.appVersion
}
}
// 计算缓存大小
const calculateCacheSize = () => {
// 这里应该计算实际缓存大小,这里使用模拟数据
cacheSize.value = '12.5 MB'
}
// 跳转到个人资料
const goToProfile = () => {
uni.navigateTo({
url: '/pages/mall/consumer/profile'
})
}
// 跳转到地址管理
const goToAddress = () => {
uni.navigateTo({
url: '/pages/mall/consumer/address'
})
}
// 修改密码
const changePassword = () => {
uni.navigateTo({
url: '/pages/user/change-password'
})
}
// 绑定手机
const bindPhone = () => {
uni.navigateTo({
url: '/pages/user/bind-phone'
})
}
// 绑定邮箱
const bindEmail = () => {
uni.navigateTo({
url: '/pages/user/bind-email'
})
}
// 切换通知设置
const toggleNotification = (type: keyof NotificationType) => {
notifications.value[type] = !notifications.value[type]
uni.setStorageSync('userNotifications', notifications.value)
}
// 切换隐私设置
const togglePrivacy = (type: keyof PrivacyType) => {
privacy.value[type] = !privacy.value[type]
uni.setStorageSync('userPrivacy', privacy.value)
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: `确定要清除 ${cacheSize.value} 缓存吗?`,
success: (res) => {
if (res.confirm) {
// 这里应该清除实际缓存
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
cacheSize.value = '0.0 MB'
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 切换语言
const changeLanguage = () => {
uni.showActionSheet({
itemList: ['简体中文', 'English', '日本語'],
success: (res) => {
const languages = ['简体中文', 'English', '日本語']
currentLanguage.value = languages[res.tapIndex]
uni.setStorageSync('appLanguage', currentLanguage.value)
uni.showToast({
title: '语言已切换',
icon: 'success'
})
}
})
}
// 切换主题
const changeTheme = () => {
uni.showActionSheet({
itemList: ['自动', '浅色模式', '深色模式'],
success: (res) => {
const themes = ['自动', '浅色模式', '深色模式']
currentTheme.value = themes[res.tapIndex]
uni.setStorageSync('appTheme', currentTheme.value)
uni.showToast({
title: '主题已切换',
icon: 'success'
})
}
})
}
// 关于我们
const aboutUs = () => {
uni.navigateTo({
url: '/pages/info/about'
})
}
// 用户协议
const userAgreement = () => {
uni.navigateTo({
url: '/pages/user/terms'
})
}
// 隐私政策
const privacyPolicy = () => {
uni.navigateTo({
url: '/pages/info/privacy'
})
}
// 检查更新
const checkUpdate = () => {
uni.showLoading({
title: '检查更新中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showModal({
title: '检查更新',
content: '当前已是最新版本',
showCancel: false
})
}, 1000)
}
// 联系客服
const contactService = () => {
uni.navigateTo({
url: '/pages/mall/service/chat'
})
}
// 意见反馈
const feedback = () => {
uni.navigateTo({
url: '/pages/info/feedback'
})
}
// 给个好评
const rateApp = () => {
// 这里应该跳转到应用商店评分
uni.showModal({
title: '给个好评',
content: '如果喜欢我们的应用,请给个好评吧!',
confirmText: '去评分',
success: (res) => {
if (res.confirm) {
// 跳转到应用商店
// @ts-ignore
uni.navigateToMiniProgram({
appId: 'wx1234567890', // 示例AppID
fail: () => {
uni.showToast({
title: '跳转失败',
icon: 'none'
})
}
})
}
}
})
}
// 退出登录
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
try {
// 调用登出接口
/*
const { error } = await supa.auth.signOut()
if (error !== null) {
console.error('登出失败:', error)
uni.showToast({
title: '登出失败',
icon: 'none'
})
return
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('userSettings')
// 跳转到登录页
uni.reLaunch({
url: '/pages/user/login'
})
} catch (err) {
console.error('登出异常:', err)
uni.showToast({
title: '登出失败',
icon: 'none'
})
}
}
}
})
}
// 注销账号
const deleteAccount = () => {
uni.showModal({
title: '注销账号',
content: '确定要注销账号吗?此操作不可恢复,所有数据将被删除!',
confirmText: '注销',
confirmColor: '#ff4757',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '处理中...'
})
try {
const userId = userInfo.value.id
// 这里应该调用注销账号的API
/*
const { error } = await supa
.from('users')
.update({ status: 0 }) // 标记为注销状态
.eq('id', userId)
if (error !== null) {
throw error
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
// 提示并跳转
uni.hideLoading()
uni.showToast({
title: '账号已注销',
icon: 'success',
duration: 2000
})
setTimeout(() => {
uni.reLaunch({
url: '/pages/user/login'
})
}, 1500)
} catch (err) {
uni.hideLoading()
console.error('注销账号失败:', err)
uni.showToast({
title: '注销失败',
icon: 'none'
})
}
}
}
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
.settings-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.settings-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.settings-content {
flex: 1;
}
.settings-section {
background-color: #ffffff;
margin-bottom: 10px;
padding: 15px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.section-list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.list-item:last-child {
border-bottom: none;
}
.item-icon {
font-size: 20px;
margin-right: 15px;
}
.item-text {
flex: 1;
font-size: 14px;
color: #333333;
}
.item-arrow {
color: #999999;
font-size: 16px;
margin-left: 10px;
}
.item-right {
display: flex;
align-items: center;
}
.item-status {
font-size: 12px;
color: #999999;
margin-right: 10px;
}
.item-status.bound {
color: #4caf50;
}
.item-cache {
font-size: 12px;
color: #999999;
margin-right: 10px;
}
.logout-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.logout-btn {
background-color: #ffffff;
color: #ff4757;
height: 45px;
border: 1px solid #ff4757;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
}
.delete-account-section {
background-color: #ffffff;
padding: 20px 15px;
text-align: center;
}
.delete-account {
color: #999999;
font-size: 14px;
text-decoration: underline;
}
</style>

View File

@@ -17,7 +17,7 @@
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="goToAddress">
<view class="list-item" @click="goToAddressList">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
@@ -192,7 +192,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
// import supa from '@/components/supadb/aksupainstance.uts'
type UserType = {
id: string
@@ -288,9 +288,9 @@ const goToProfile = () => {
}
// 跳转到地址管理
const goToAddress = () => {
const goToAddressList = () => {
uni.navigateTo({
url: '/pages/mall/consumer/address'
url: '/pages/mall/consumer/address-list'
})
}
@@ -471,6 +471,7 @@ const logout = () => {
if (res.confirm) {
try {
// 调用登出接口
/*
const { error } = await supa.auth.signOut()
if (error !== null) {
@@ -481,6 +482,7 @@ const logout = () => {
})
return
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
@@ -521,6 +523,7 @@ const deleteAccount = () => {
const userId = userInfo.value.id
// 这里应该调用注销账号的API
/*
const { error } = await supa
.from('users')
.update({ status: 0 }) // 标记为注销状态
@@ -529,6 +532,7 @@ const deleteAccount = () => {
if (error !== null) {
throw error
}
*/
// 清除本地存储
uni.removeStorageSync('userInfo')
@@ -568,6 +572,51 @@ const goBack = () => {
</script>
<style scoped>
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.settings-content {
padding: 20px;
background-color: #f5f5f5;
}
.settings-section {
border-radius: 8px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
/* 电脑端横向排列部分内容 */
.section-list {
display: flex;
flex-direction: column;
}
.logout-section, .delete-account-section {
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
}
@media screen and (min-width: 1024px) {
.settings-page {
flex-direction: row; /* 大屏下改为横向布局,左侧导航,右侧内容 */
}
.settings-header {
display: none; /* 大屏下隐藏顶部栏,可能使用侧边栏或其他导航 */
}
/* 这里只是简单示例,实际可能需要更复杂的布局调整 */
.settings-content {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
}
.settings-page {
display: flex;
flex-direction: column;

View File

@@ -0,0 +1,950 @@
<!-- 钱包页面 -->
<template>
<view class="wallet-page">
<!-- 顶部栏 -->
<view class="wallet-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">我的钱包</text>
<text class="more-btn" @click="showMoreActions">···</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>
.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;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.more-btn {
color: #333333;
font-size: 20px;
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>

View File

@@ -486,6 +486,54 @@ const goBack = () => {
</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;