feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
87
src_bak/pages/points/exchange.tsx
Normal file
87
src_bak/pages/points/exchange.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useAddress } from '@/hooks/useAddress'
|
||||
import AddressCard from '@/components/business/AddressCard'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '确认兑换',
|
||||
})
|
||||
|
||||
const ExchangePage: React.FC = () => {
|
||||
const { defaultAddress } = useAddress()
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 flex flex-col" style={{ minHeight: '100vh' }}>
|
||||
<View className='flex-1'>
|
||||
{/* 收货地址 */}
|
||||
<View
|
||||
className="bg-white p-3"
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/address-list' })}
|
||||
>
|
||||
{defaultAddress ? (
|
||||
<AddressCard address={defaultAddress} />
|
||||
) : (
|
||||
<View className="flex items-center justify-center py-6">
|
||||
<Text className="text-sm text-gray-400">请点击添加收货地址</Text>
|
||||
<Text className="text-gray-300 ml-1">→</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 商品信息(确认页需从上一页传递数据,此处为占位) */}
|
||||
<View className="bg-white mt-2 p-3">
|
||||
<View className="flex gap-3 py-2">
|
||||
<View className="w-16 h-16 rounded-md bg-orange-50 flex items-center justify-center">
|
||||
<Text className="text-xs text-gray-400">商品图</Text>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-sm text-gray-700 block mb-1">积分兑换商品</Text>
|
||||
<Text className="text-xs text-gray-400 block">x1</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-bold text-orange-500">500 积分</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 费用明细 */}
|
||||
<View className="bg-white mt-2 p-3">
|
||||
<Text className="text-sm font-medium text-gray-800 mb-2 block">费用明细</Text>
|
||||
<View className="flex justify-between py-1">
|
||||
<Text className="text-sm text-gray-500">商品积分</Text>
|
||||
<Text className="text-sm text-gray-700">500 积分</Text>
|
||||
</View>
|
||||
<View className="flex justify-between py-1">
|
||||
<Text className="text-sm text-gray-500">运费</Text>
|
||||
<Text className="text-sm text-green-600">包邮</Text>
|
||||
</View>
|
||||
<View className="flex justify-between py-1 mt-1 border-t border-gray-50">
|
||||
<Text className="text-sm font-medium text-gray-800">合计</Text>
|
||||
<Text className="text-sm font-bold text-orange-500">500 积分</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部栏 */}
|
||||
<View className="bg-white border-t border-gray-100 px-4 py-3 flex items-center justify-between" style={{ paddingBottom: '20px' }}>
|
||||
<View>
|
||||
<Text className="text-xs text-gray-500">需支付:</Text>
|
||||
<Text className="text-lg font-bold text-orange-500">500 积分</Text>
|
||||
</View>
|
||||
<View
|
||||
className="px-8 py-2 rounded-full"
|
||||
style={{ backgroundColor: '#f97316' }}
|
||||
onClick={() => {
|
||||
Taro.showToast({ title: '兑换成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateTo({ url: '/pages/order-list' }), 1500)
|
||||
}}
|
||||
>
|
||||
<Text className="text-white font-medium text-sm">确认兑换</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExchangePage
|
||||
4
src_bak/pages/points/exchange/index.config.ts
Normal file
4
src_bak/pages/points/exchange/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '积分兑换',
|
||||
enablePullDownRefresh: true,
|
||||
}
|
||||
226
src_bak/pages/points/exchange/index.tsx
Normal file
226
src_bak/pages/points/exchange/index.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useRequest } from '@/hooks/useRequest'
|
||||
import { listShopGoods, type ShopGoods } from '@/api/shop/shopGoods'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '积分兑换',
|
||||
})
|
||||
|
||||
// 兑换分类
|
||||
const categories = [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: '优惠券', value: 1 },
|
||||
{ label: '实物商品', value: 2 },
|
||||
{ label: '虚拟商品', value: 3 },
|
||||
]
|
||||
|
||||
const ExchangePage: React.FC = () => {
|
||||
const [activeCat, setActiveCat] = useState(0)
|
||||
const [userPoints] = useState(1280) // 模拟用户积分
|
||||
|
||||
// 获取可兑换商品
|
||||
const { data, loading } = useRequest(listShopGoods, {
|
||||
defaultParams: [{ page: 1, limit: 20, exchangeType: 1 }], // exchangeType=1 表示积分兑换商品
|
||||
onError: (err) => {
|
||||
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟兑换商品数据(实际应该从API获取)
|
||||
const exchangeProducts = [
|
||||
{
|
||||
id: 1,
|
||||
name: '满100减10优惠券',
|
||||
points: 500,
|
||||
image: '',
|
||||
stock: 100,
|
||||
type: 1,
|
||||
description: '积分兑换专属优惠券',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '满200减25优惠券',
|
||||
points: 1000,
|
||||
image: '',
|
||||
stock: 50,
|
||||
type: 1,
|
||||
description: '积分兑换专属优惠券',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '品牌定制水杯',
|
||||
points: 2000,
|
||||
image: '',
|
||||
stock: 30,
|
||||
type: 2,
|
||||
description: '高品质保温水杯,限量兑换',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '精美帆布袋',
|
||||
points: 1500,
|
||||
image: '',
|
||||
stock: 50,
|
||||
type: 2,
|
||||
description: '环保帆布袋,时尚实用',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '视频会员月卡',
|
||||
points: 3000,
|
||||
image: '',
|
||||
stock: 20,
|
||||
type: 3,
|
||||
description: '主流视频平台会员月卡',
|
||||
},
|
||||
]
|
||||
|
||||
// 筛选商品
|
||||
const filteredProducts = activeCat === 0
|
||||
? exchangeProducts
|
||||
: exchangeProducts.filter(p => p.type === activeCat)
|
||||
|
||||
// 处理兑换
|
||||
const handleExchange = (product: typeof exchangeProducts[0]) => {
|
||||
if (userPoints < product.points) {
|
||||
Taro.showToast({ title: '积分不足', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (product.stock <= 0) {
|
||||
Taro.showToast({ title: '库存不足', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
Taro.showModal({
|
||||
title: '确认兑换',
|
||||
content: `确定使用 ${product.points} 积分兑换「${product.name}」吗?`,
|
||||
confirmText: '确认兑换',
|
||||
confirmColor: '#0e932e',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.showLoading({ title: '兑换中...' })
|
||||
// 这里应该调用兑换API
|
||||
setTimeout(() => {
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({ title: '兑换成功', icon: 'success' })
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeLabel = (type: number) => {
|
||||
const map: Record<number, string> = { 1: '优惠券', 2: '实物商品', 3: '虚拟商品' }
|
||||
return map[type] || '其他'
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* 用户积分信息 */}
|
||||
<View className='p-4' style={{ background: 'linear-gradient(to right, #fb923c, #ef4444)' }}>
|
||||
<View className='rounded-xl p-4' style={{ backgroundColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<Text className='text-white text-sm block mb-2'>我的积分</Text>
|
||||
<View className='flex items-baseline gap-1'>
|
||||
<Text className='text-white text-4xl font-bold'>{userPoints}</Text>
|
||||
<Text className='text-white text-sm opacity-80'>积分</Text>
|
||||
</View>
|
||||
<View className='flex gap-4 mt-3'>
|
||||
<View className='flex-1 rounded-lg p-2 text-center' style={{ backgroundColor: 'rgba(255,255,255,0.3)' }}>
|
||||
<Text className='text-white text-xs block'>已兑换</Text>
|
||||
<Text className='text-white text-sm font-bold block mt-1'>3 件</Text>
|
||||
</View>
|
||||
<View className='flex-1 rounded-lg p-2 text-center' style={{ backgroundColor: 'rgba(255,255,255,0.3)' }}>
|
||||
<Text className='text-white text-xs block'>兑换记录</Text>
|
||||
<Text className='text-white text-sm font-bold block mt-1'>查看</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<View className='bg-white flex overflow-x-auto'>
|
||||
{categories.map(cat => (
|
||||
<View
|
||||
key={cat.value}
|
||||
className={`flex-shrink-0 px-4 py-3 relative ${
|
||||
activeCat === cat.value ? 'text-orange-500 font-medium' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => setActiveCat(cat.value)}
|
||||
>
|
||||
<Text className='text-sm'>{cat.label}</Text>
|
||||
{activeCat === cat.value && (
|
||||
<View className='absolute bottom-0 left-0 right-0 flex justify-center'>
|
||||
<View className='w-8 h-px bg-orange-500 rounded' />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 商品列表 */}
|
||||
<ScrollView className='flex-1'>
|
||||
{filteredProducts.length === 0 ? (
|
||||
<View className='text-center py-16'>
|
||||
<Text className='text-4xl mb-3 block'>🎁</Text>
|
||||
<Text className='text-sm text-gray-400'>暂无兑换商品</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='p-3'>
|
||||
{filteredProducts.map(product => {
|
||||
const canExchange = userPoints >= product.points && product.stock > 0
|
||||
return (
|
||||
<View key={product.id} className='bg-white rounded-xl p-4 mb-3 shadow-sm'>
|
||||
{/* 商品信息 */}
|
||||
<View className='flex gap-3'>
|
||||
{/* 商品图片 */}
|
||||
<View className='w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0'>
|
||||
<Text className='text-3xl'>🎁</Text>
|
||||
</View>
|
||||
|
||||
{/* 商品详情 */}
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-800 font-medium block mb-1'>{product.name}</Text>
|
||||
<Text className='text-xs text-gray-400 block mb-2'>{product.description}</Text>
|
||||
|
||||
<View className='flex items-center gap-2'>
|
||||
<View className='bg-orange-50 px-2 py-1 rounded'>
|
||||
<Text className='text-xs text-orange-500 font-bold'>{product.points} 积分</Text>
|
||||
</View>
|
||||
<View className='bg-blue-50 px-2 py-1 rounded'>
|
||||
<Text className='text-xs text-blue-500'>{getTypeLabel(product.type)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='flex justify-between items-center mt-2'>
|
||||
<Text className='text-xs text-gray-400'>库存: {product.stock}</Text>
|
||||
<View
|
||||
className={`px-3 py-1 rounded-full ${
|
||||
canExchange ? 'bg-orange-500' : 'bg-gray-300'
|
||||
}`}
|
||||
onClick={() => canExchange && handleExchange(product)}
|
||||
>
|
||||
<Text className='text-xs text-white'>
|
||||
{!canExchange
|
||||
? userPoints < product.points ? '积分不足' : '库存不足'
|
||||
: '立即兑换'
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExchangePage
|
||||
3
src_bak/pages/points/index.config.ts
Normal file
3
src_bak/pages/points/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '积分兑换',
|
||||
}
|
||||
116
src_bak/pages/points/index.tsx
Normal file
116
src_bak/pages/points/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { pageShopPointsProduct } from '@/api/shop/shopPointsProduct'
|
||||
import type { ShopPointsProduct } from '@/api/shop/shopPointsProduct/model'
|
||||
import { getUserPointsInfo, type UserPointsInfo } from '@/api/system/user/points'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
const PointsPage: React.FC = () => {
|
||||
const { user } = useUser()
|
||||
const [pointsInfo, setPointsInfo] = useState<UserPointsInfo>({ points: 0, totalEarned: 0, totalUsed: 0, expiringSoon: 0 })
|
||||
const [hotProducts, setHotProducts] = useState<ShopPointsProduct[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 获取当前用户真实积分
|
||||
getUserPointsInfo()
|
||||
.then(data => setPointsInfo(data))
|
||||
.catch(err => console.error('获取积分信息失败:', err))
|
||||
|
||||
pageShopPointsProduct({ page: 1, limit: 4, isHot: 1, status: 1 })
|
||||
.then(res => setHotProducts(res?.list || []))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='min-h-screen bg-gray-50'>
|
||||
{/* 积分卡片 */}
|
||||
<View className='mx-3 mt-3 p-4 rounded-xl' style={{ background: 'linear-gradient(135deg, #0e932e, #16a34a)' }}>
|
||||
<Text className='text-white text-sm opacity-80 block mb-1'>当前积分</Text>
|
||||
<Text className='text-white text-3xl font-bold block'>{pointsInfo.points}</Text>
|
||||
<View className='flex gap-3 mt-4'>
|
||||
<View
|
||||
className='flex-1 py-2 rounded-full text-center'
|
||||
style={{ backgroundColor: 'rgba(255,255,255,0.2)' }}
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/points/signin' })}
|
||||
>
|
||||
<Text className='text-white text-sm'>每日签到</Text>
|
||||
</View>
|
||||
<View
|
||||
className='flex-1 py-2 rounded-full text-center'
|
||||
style={{ backgroundColor: 'rgba(255,255,255,0.2)' }}
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/user/points-record' })}
|
||||
>
|
||||
<Text className='text-white text-sm'>积分明细</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 热门兑换 */}
|
||||
<View className='mx-3 mt-4'>
|
||||
<View className='flex justify-between items-center mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800'>热门兑换</Text>
|
||||
<Text
|
||||
className='text-xs text-gray-400'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/points/product-list/index' })}
|
||||
>{`查看更多 >`}</Text>
|
||||
</View>
|
||||
|
||||
{!loading && hotProducts.length === 0 ? (
|
||||
<EmptyState text='暂无热门商品' />
|
||||
) : (
|
||||
<View className='grid grid-cols-2 gap-2'>
|
||||
{loading
|
||||
? [1, 2, 3, 4].map(i => (
|
||||
<View key={i} className='bg-white rounded-lg overflow-hidden'>
|
||||
<View className='w-full aspect-square bg-gray-100' />
|
||||
<View className='p-2'>
|
||||
<View className='h-4 bg-gray-100 rounded mb-2' />
|
||||
<View className='h-3 bg-gray-100 rounded w-16' />
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
: hotProducts.map(item => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='bg-white rounded-lg overflow-hidden'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/points/product-detail?id=${item.id}` })}
|
||||
>
|
||||
<View className='w-full aspect-square bg-orange-50 flex items-center justify-center overflow-hidden'>
|
||||
{item.productImage ? (
|
||||
<Image src={item.productImage} className='w-full h-full' mode='aspectFill' />
|
||||
) : (
|
||||
<Text className='text-xs text-gray-300'>暂无图片</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='p-2'>
|
||||
<Text className='text-sm text-gray-700 truncate block'>{item.productName}</Text>
|
||||
<View className='flex items-center gap-1 mt-1'>
|
||||
<Text className='text-sm font-bold text-orange-500'>{item.pointsPrice || 0}积分</Text>
|
||||
{Number(item.moneyPrice) > 0 && (
|
||||
<Text className='text-xs text-gray-400'>+¥{item.moneyPrice}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 积分订单入口 */}
|
||||
<View
|
||||
className='mx-3 mt-4 mb-4 bg-white rounded-lg p-4 flex justify-between items-center'
|
||||
onClick={() => Taro.navigateTo({ url: '/pages/order-list' })}
|
||||
>
|
||||
<Text className='text-sm text-gray-700'>积分订单</Text>
|
||||
<Text className='text-gray-400 text-sm'>{'>'}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsPage
|
||||
222
src_bak/pages/points/order-list/index.tsx
Normal file
222
src_bak/pages/points/order-list/index.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listShopPointsOrder, cancelShopPointsOrder } from '@/api/shop/shopPointsOrder'
|
||||
import type { ShopPointsOrder, PointsOrderStatus } from '@/api/shop/shopPointsOrder'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '积分订单',
|
||||
})
|
||||
|
||||
// Tab 配置
|
||||
const TAB_LIST = [
|
||||
{ title: '全部', status: undefined },
|
||||
{ title: '待发货', status: 'paid' as PointsOrderStatus },
|
||||
{ title: '已完成', status: 'completed' as PointsOrderStatus },
|
||||
{ title: '已取消', status: 'cancelled' as PointsOrderStatus },
|
||||
]
|
||||
|
||||
const PointsOrderListPage: React.FC = () => {
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [orders, setOrders] = useState<ShopPointsOrder[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setOrders([])
|
||||
setPage(1)
|
||||
setFinished(false)
|
||||
loadList(1)
|
||||
}, [tabIndex])
|
||||
|
||||
const loadList = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const status = TAB_LIST[tabIndex]?.status
|
||||
const params: any = {
|
||||
page: p,
|
||||
limit: 10,
|
||||
}
|
||||
if (status) {
|
||||
params.status = status
|
||||
}
|
||||
|
||||
const res = await listShopPointsOrder(params)
|
||||
|
||||
if (res?.list) {
|
||||
if (p === 1) {
|
||||
setOrders(res.list)
|
||||
} else {
|
||||
setOrders(prev => [...prev, ...res.list])
|
||||
}
|
||||
setFinished(res.list.length < 10)
|
||||
setPage(p)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载积分订单失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadList(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
const handleCancel = async (id: string) => {
|
||||
Taro.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定取消该订单吗?',
|
||||
confirmColor: '#0e932e',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await cancelShopPointsOrder(id)
|
||||
Taro.showToast({ title: '已取消', icon: 'success' })
|
||||
// 重新加载列表
|
||||
setOrders([])
|
||||
setPage(1)
|
||||
setFinished(false)
|
||||
loadList(1)
|
||||
} catch (err) {
|
||||
console.error('取消订单失败', err)
|
||||
Taro.showToast({ title: '取消失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleDetail = (id: string) => {
|
||||
Taro.navigateTo({ url: `/pages/points/order-detail/index?id=${id}` })
|
||||
}
|
||||
|
||||
// 获取状态文本和颜色
|
||||
const getStatusInfo = (status: PointsOrderStatus) => {
|
||||
const map: Record<PointsOrderStatus, { label: string; color: string }> = {
|
||||
'pending': { label: '待支付', color: 'text-orange-500' },
|
||||
'paid': { label: '待发货', color: 'text-blue-500' },
|
||||
'shipped': { label: '已发货', color: 'text-blue-500' },
|
||||
'completed': { label: '已完成', color: 'text-green-500' },
|
||||
'cancelled': { label: '已取消', color: 'text-gray-400' },
|
||||
}
|
||||
return map[status] || { label: '未知', color: 'text-gray-400' }
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
{/* Tab 栏 */}
|
||||
<View className='bg-white flex'>
|
||||
{TAB_LIST.map((tab, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`flex-1 text-center py-3 relative ${
|
||||
tabIndex === index ? 'text-orange-500 font-medium' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => setTabIndex(index)}
|
||||
>
|
||||
<Text className='text-sm'>{tab.title}</Text>
|
||||
{tabIndex === index && (
|
||||
<View className='absolute bottom-0 w-8 h-px bg-orange-500 rounded' style={{ left: '50%', transform: 'translateX(-50%)' }} />
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 订单列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{orders.length > 0 ? (
|
||||
orders.map(order => {
|
||||
const statusInfo = getStatusInfo(order.status)
|
||||
return (
|
||||
<View
|
||||
key={order.id}
|
||||
className='bg-white rounded-xl p-4 mb-3'
|
||||
onClick={() => handleDetail(order.id)}
|
||||
>
|
||||
{/* 顶部状态 */}
|
||||
<View className='flex justify-between items-center mb-3'>
|
||||
<Text className='text-xs text-gray-500'>{order.orderNo}</Text>
|
||||
<Text className={`text-sm font-medium ${statusInfo.color}`}>
|
||||
{statusInfo.label}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 商品列表 */}
|
||||
{order.items.map((item, idx) => (
|
||||
<View key={idx} className='flex items-center gap-2 mb-2'>
|
||||
<View className='w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0'>
|
||||
<Image
|
||||
src={item.goodsImage}
|
||||
className='w-10 h-10 rounded'
|
||||
mode='aspectFill'
|
||||
/>
|
||||
</View>
|
||||
<View className='flex-1'>
|
||||
<Text className='text-sm text-gray-800 font-medium block'>{item.goodsName}</Text>
|
||||
<Text className='text-xs text-gray-500 mt-0 block'>
|
||||
{item.points}积分 x {item.quantity}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 底部操作 */}
|
||||
<View className='flex justify-between items-center pt-2 border-t border-gray-50'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
共 {order.totalPoints} 积分
|
||||
</Text>
|
||||
<View className='flex gap-2'>
|
||||
{order.status === 'paid' && (
|
||||
<View
|
||||
className='text-center py-1 px-3 rounded-full border border-red-500'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCancel(order.id)
|
||||
}}
|
||||
>
|
||||
<Text className='text-xs text-red-500'>取消订单</Text>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
className='text-center py-1 px-3 rounded-full bg-orange-500'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDetail(order.id)
|
||||
}}
|
||||
>
|
||||
<Text className='text-xs text-white'>查看详情</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
) : !loading ? (
|
||||
<EmptyState text='暂无积分订单' />
|
||||
) : null}
|
||||
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsOrderListPage
|
||||
168
src_bak/pages/points/points-rules/index.tsx
Normal file
168
src_bak/pages/points/points-rules/index.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { useScrollHeight } from '@/hooks/useScrollHeight'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '积分规则',
|
||||
})
|
||||
|
||||
const PointsRulesPage: React.FC = () => {
|
||||
const scrollHeight = useScrollHeight(44)
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<ScrollView scrollY style={{ height: scrollHeight }}>
|
||||
{/* 积分获取规则 */}
|
||||
<View className='mx-3 mt-3 bg-white rounded-xl p-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>积分获取</Text>
|
||||
|
||||
<View className='mb-4'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>📅</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>每日签到</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 每日签到可获得 5 积分</Text>
|
||||
<Text className='block'>• 连续签到 7 天额外获得 50 积分</Text>
|
||||
<Text className='block'>• 连续签到 30 天额外获得 200 积分</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='mb-4'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>🛍️</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>购物获得</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 每消费 1 元可获得 1 积分</Text>
|
||||
<Text className='block'>• 使用积分抵扣金额不获得积分</Text>
|
||||
<Text className='block'>• 退款时扣除相应积分</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='mb-4'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>✍️</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>评价获得</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 订单完成后评价可获得 10 积分</Text>
|
||||
<Text className='block'>• 上传图片评价额外获得 5 积分</Text>
|
||||
<Text className='block'>• 每条订单仅限评价一次</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>🎁</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>活动赠送</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 注册赠送 50 积分</Text>
|
||||
<Text className='block'>• 完善个人信息赠送 20 积分</Text>
|
||||
<Text className='block'>• 节日活动、生日赠送等</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 积分使用规则 */}
|
||||
<View className='mx-3 mt-3 bg-white rounded-xl p-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>积分使用</Text>
|
||||
|
||||
<View className='mb-4'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>💰</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>抵扣金额</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 100 积分可抵扣 1 元</Text>
|
||||
<Text className='block'>• 最高可抵扣订单金额的 10%</Text>
|
||||
<Text className='block'>• 抵扣金额不可超过订单实付金额</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='mb-4'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>🎫</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>兑换优惠券</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 可在积分商城兑换各类优惠券</Text>
|
||||
<Text className='block'>• 兑换后不可撤销</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-lg'>🎁</Text>
|
||||
<Text className='text-sm font-medium text-gray-700'>兑换商品</Text>
|
||||
</View>
|
||||
<View className='text-xs text-gray-500 leading-5 pl-6'>
|
||||
<Text className='block'>• 可在积分商城兑换实物商品</Text>
|
||||
<Text className='block'>• 兑换后不可撤销</Text>
|
||||
<Text className='block'>• 商品兑换需支付邮费</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 积分有效期规则 */}
|
||||
<View className='mx-3 mt-3 bg-white rounded-xl p-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>积分有效期</Text>
|
||||
|
||||
<View className='text-xs text-gray-500 leading-5'>
|
||||
<Text className='block mb-2'>• 积分有效期为获得之日起 1 年</Text>
|
||||
<Text className='block mb-2'>• 过期积分将自动扣除</Text>
|
||||
<Text className='block mb-2'>• 可在"积分明细"中查看即将过期的积分</Text>
|
||||
<Text className='block'>• 建议使用即将过期的积分</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 积分等级规则 */}
|
||||
<View className='mx-3 mt-3 bg-white rounded-xl p-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>会员等级</Text>
|
||||
|
||||
<View className='mb-3'>
|
||||
<View className='flex justify-between py-2 border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-600'>普通会员</Text>
|
||||
<Text className='text-xs text-gray-400'>0-999 积分</Text>
|
||||
</View>
|
||||
<View className='flex justify-between py-2 border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-600'>银卡会员</Text>
|
||||
<Text className='text-xs text-gray-400'>1000-4999 积分</Text>
|
||||
</View>
|
||||
<View className='flex justify-between py-2 border-b border-gray-50'>
|
||||
<Text className='text-sm text-gray-600'>金卡会员</Text>
|
||||
<Text className='text-xs text-gray-400'>5000-9999 积分</Text>
|
||||
</View>
|
||||
<View className='flex justify-between py-2'>
|
||||
<Text className='text-sm text-gray-600'>钻石会员</Text>
|
||||
<Text className='text-xs text-gray-400'>10000 积分以上</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='text-xs text-gray-400 leading-5'>
|
||||
<Text className='block'>• 会员等级根据历史累计获得积分计算</Text>
|
||||
<Text className='block'>• 等级越高,享受的折扣和特权越多</Text>
|
||||
<Text className='block'>• 会员等级每月 1 号更新</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 其他说明 */}
|
||||
<View className='mx-3 mt-3 mb-3 bg-white rounded-xl p-4'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>其他说明</Text>
|
||||
|
||||
<View className='text-xs text-gray-500 leading-5'>
|
||||
<Text className='block mb-2'>1. 积分不可转让、不可兑现</Text>
|
||||
<Text className='block mb-2'>2. 账户注销后积分将清零</Text>
|
||||
<Text className='block mb-2'>3. 如有疑问,请联系客服</Text>
|
||||
<Text className='block'>4. 平台保留最终解释权</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='h-6' />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsRulesPage
|
||||
177
src_bak/pages/points/product-detail.tsx
Normal file
177
src_bak/pages/points/product-detail.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, ScrollView, RichText } from '@tarojs/components'
|
||||
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
|
||||
import { getShopPointsProduct } from '@/api/shop/shopPointsProduct'
|
||||
import type { ShopPointsProduct } from '@/api/shop/shopPointsProduct/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '积分商品',
|
||||
})
|
||||
|
||||
const PointsProductDetail: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const id = Number(router.params.id)
|
||||
const [product, setProduct] = useState<ShopPointsProduct | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// 微信分享
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: product?.productName || '积分商品推荐',
|
||||
path: `/pages/points/product-detail?id=${id}`,
|
||||
imageUrl: product?.productImage || ''
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadProduct()
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const loadProduct = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await getShopPointsProduct(id)
|
||||
setProduct(data)
|
||||
} catch (e) {
|
||||
console.error('加载积分商品失败:', e)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getProductTypeLabel = (type?: number) => {
|
||||
switch (type) {
|
||||
case 0: return '实物商品'
|
||||
case 1: return '优惠券'
|
||||
case 2: return '虚拟商品'
|
||||
default: return '商品'
|
||||
}
|
||||
}
|
||||
|
||||
const handleExchange = () => {
|
||||
if (!product) return
|
||||
if ((product.stock || 0) <= 0) {
|
||||
Taro.showToast({ title: '商品已售罄', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 跳转兑换确认页,传递商品信息
|
||||
Taro.navigateTo({
|
||||
url: `/pages/points/exchange/index?id=${product.id}`
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<View className='w-full bg-orange-50 animate-pulse' style={{ height: '375px' }} />
|
||||
<View className='bg-white p-4'>
|
||||
<View className='h-6 bg-gray-200 rounded mb-2 animate-pulse' style={{ width: '70%' }} />
|
||||
<View className='h-8 bg-gray-200 rounded mb-2 animate-pulse' style={{ width: '40%' }} />
|
||||
<View className='h-4 bg-gray-200 rounded animate-pulse' style={{ width: '55%' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
<EmptyState icon='🎁' text='商品不存在或已下架' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const isSoldOut = (product.stock || 0) <= 0
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 商品图片 */}
|
||||
{product.productImage ? (
|
||||
<Image
|
||||
className='w-full'
|
||||
style={{ height: '375px' }}
|
||||
src={product.productImage}
|
||||
mode='aspectFill'
|
||||
onClick={() => Taro.previewImage({ current: product.productImage, urls: [product.productImage!] })}
|
||||
/>
|
||||
) : (
|
||||
<View className='w-full bg-orange-50 flex items-center justify-center' style={{ height: '375px' }}>
|
||||
<Text className='text-gray-400 text-sm'>暂无图片</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 价格与基本信息 */}
|
||||
<View className='bg-white p-4'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<View className='px-2 rounded' style={{ backgroundColor: '#fff7ed', paddingTop: '2px', paddingBottom: '2px' }}>
|
||||
<Text className='text-xs text-orange-500'>{getProductTypeLabel(product.productType)}</Text>
|
||||
</View>
|
||||
{product.isHot === 1 && (
|
||||
<View className='px-2 rounded' style={{ backgroundColor: '#fef2f2', paddingTop: '2px', paddingBottom: '2px' }}>
|
||||
<Text className='text-xs text-red-500'>热门</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text className='text-base font-bold text-gray-800 block leading-tight'>
|
||||
{product.productName || '积分商品'}
|
||||
</Text>
|
||||
<View className='flex items-baseline gap-1 mt-2'>
|
||||
<Text className='text-xs text-orange-500'>需</Text>
|
||||
<Text className='text-2xl font-bold text-orange-500'>
|
||||
{product.pointsPrice || 0}
|
||||
</Text>
|
||||
<Text className='text-xs text-orange-500'>积分</Text>
|
||||
{Number(product.moneyPrice) > 0 && (
|
||||
<>
|
||||
<Text className='text-xs text-gray-500 ml-1'>+</Text>
|
||||
<Text className='text-base font-medium text-orange-500 ml-1'>
|
||||
{'\u00A5'}{product.moneyPrice}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<View className='flex items-center gap-4 mt-2'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
已兑换 {product.sales || 0} 件
|
||||
</Text>
|
||||
<Text className={`text-xs ${isSoldOut ? 'text-red-400' : 'text-green-500'}`}>
|
||||
{isSoldOut ? '已售罄' : `剩余 ${product.stock || 0} 件`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 商品描述 */}
|
||||
{product.description && (
|
||||
<View className='bg-white mt-2 p-4'>
|
||||
<Text className='text-sm font-medium text-gray-800 block mb-3'>商品介绍</Text>
|
||||
<RichText
|
||||
className='text-sm text-gray-600 leading-relaxed'
|
||||
nodes={product.description}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 底部占位,防止被固定栏遮挡 */}
|
||||
<View style={{ height: '80px' }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部兑换按钮 */}
|
||||
<View className='bg-white border-t border-gray-100 px-4 py-3 flex items-center justify-center' style={{ paddingBottom: '20px' }}>
|
||||
<View
|
||||
className='w-full py-3 rounded-full text-center text-white font-medium'
|
||||
style={{ backgroundColor: isSoldOut ? '#d1d5db' : '#f97316' }}
|
||||
onClick={handleExchange}
|
||||
>
|
||||
<Text className='text-sm'>{isSoldOut ? '已售罄' : '立即兑换'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsProductDetail
|
||||
3
src_bak/pages/points/product-list/index.config.ts
Normal file
3
src_bak/pages/points/product-list/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '积分商城',
|
||||
}
|
||||
170
src_bak/pages/points/product-list/index.tsx
Normal file
170
src_bak/pages/points/product-list/index.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { listShopPointsProduct, pageShopPointsProduct } from '@/api/shop/shopPointsProduct'
|
||||
import type { ShopPointsProduct } from '@/api/shop/shopPointsProduct/model'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '积分商城',
|
||||
})
|
||||
|
||||
const PointsProductListPage: React.FC = () => {
|
||||
const [products, setProducts] = useState<ShopPointsProduct[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const limit = 20
|
||||
|
||||
useEffect(() => {
|
||||
loadProducts(true)
|
||||
}, [])
|
||||
|
||||
const loadProducts = async (reset = false) => {
|
||||
const currentPage = reset ? 1 : page
|
||||
if (reset) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await pageShopPointsProduct({
|
||||
page: currentPage,
|
||||
limit,
|
||||
status: 1,
|
||||
})
|
||||
|
||||
if (reset) {
|
||||
setProducts(data?.list || [])
|
||||
} else {
|
||||
setProducts(prev => [...prev, ...(data?.list || [])])
|
||||
}
|
||||
setHasMore((data?.list || []).length >= limit)
|
||||
setPage(currentPage + 1)
|
||||
} catch (e) {
|
||||
console.error('加载积分商品失败:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!loadingMore && hasMore) {
|
||||
loadProducts(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getProductTypeLabel = (type?: number) => {
|
||||
switch (type) {
|
||||
case 0: return '实物'
|
||||
case 1: return '优惠券'
|
||||
case 2: return '虚拟'
|
||||
default: return '商品'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{loading ? (
|
||||
<View className='p-3'>
|
||||
<View className='grid grid-cols-2 gap-2'>
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<View key={i} className='bg-white rounded-lg overflow-hidden animate-pulse'>
|
||||
<View className='w-full aspect-square bg-gray-200' />
|
||||
<View className='p-2'>
|
||||
<View className='h-4 bg-gray-200 rounded mb-2' style={{ width: '75%' }} />
|
||||
<View className='h-3 bg-gray-200 rounded' style={{ width: '50%' }} />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : products.length === 0 ? (
|
||||
<EmptyState
|
||||
icon='🎁'
|
||||
text='暂无积分商品'
|
||||
subText='平台正在准备更多好物,敬请期待'
|
||||
/>
|
||||
) : (
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
>
|
||||
<View className='p-3'>
|
||||
<View className='grid grid-cols-2 gap-2'>
|
||||
{products.map(product => (
|
||||
<View
|
||||
key={product.id}
|
||||
className='bg-white rounded-lg overflow-hidden'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/points/product-detail?id=${product.id}` })}
|
||||
>
|
||||
<View className='w-full aspect-square bg-orange-50 relative'>
|
||||
<Image
|
||||
className='w-full h-full'
|
||||
src={product.productImage || ''}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='absolute top-1 right-1 px-2 rounded-full bg-black bg-opacity-50' style={{ paddingTop: '2px', paddingBottom: '2px' }}>
|
||||
<Text className='text-white text-xs'>{getProductTypeLabel(product.productType)}</Text>
|
||||
</View>
|
||||
{product.isHot === 1 && (
|
||||
<View className='absolute top-1 left-1 px-2 rounded-br-lg' style={{ backgroundColor: '#f97316', paddingTop: '2px', paddingBottom: '2px' }}>
|
||||
<Text className='text-white text-xs'>热门</Text>
|
||||
</View>
|
||||
)}
|
||||
{product.stock === 0 && (
|
||||
<View className='absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center'>
|
||||
<Text className='text-white font-medium'>已售罄</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='p-2'>
|
||||
<Text className='text-sm text-gray-700 line-clamp-2 block'>
|
||||
{product.productName || '积分商品'}
|
||||
</Text>
|
||||
<View className='flex items-center gap-1 mt-1'>
|
||||
<Text className='text-base font-bold text-orange-500'>
|
||||
{product.pointsPrice || 0}
|
||||
</Text>
|
||||
<Text className='text-xs text-orange-500'>积分</Text>
|
||||
{Number(product.moneyPrice) > 0 && (
|
||||
<Text className='text-xs text-gray-400 ml-1'>
|
||||
+¥{product.moneyPrice}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='flex justify-between items-center mt-1'>
|
||||
<Text className='text-xs text-gray-400'>
|
||||
已兑换 {product.sales || 0}
|
||||
</Text>
|
||||
<Text className={`text-xs ${(product.stock || 0) > 0 ? 'text-green-500' : 'text-gray-400'}`}>
|
||||
库存 {product.stock || 0}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<LoadMore loading={loadingMore} />
|
||||
)}
|
||||
{!hasMore && products.length > 0 && (
|
||||
<View className='py-6 text-center'>
|
||||
<Text className='text-xs text-gray-400'>— 没有更多了 —</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsProductListPage
|
||||
149
src_bak/pages/points/signin.tsx
Normal file
149
src_bak/pages/points/signin.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
import { listShopSigninRecord, addShopSigninRecord } from '@/api/shop/shopSigninRecord'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '每日签到',
|
||||
})
|
||||
|
||||
const SigninPage: React.FC = () => {
|
||||
const { user, isLoggedIn } = useUser()
|
||||
const [signedDates, setSignedDates] = useState<string[]>([])
|
||||
const [todaySigned, setTodaySigned] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const today = new Date()
|
||||
const todayStr = today.toISOString().split('T')[0] // YYYY-MM-DD
|
||||
|
||||
// 获取当月已签到日期
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/passport/login' })
|
||||
return
|
||||
}
|
||||
loadSignedRecords()
|
||||
}, [isLoggedIn])
|
||||
|
||||
const loadSignedRecords = async () => {
|
||||
try {
|
||||
const year = today.getFullYear()
|
||||
const month = today.getMonth() + 1
|
||||
const data = await listShopSigninRecord({
|
||||
userId: (user as any)?.userId,
|
||||
year,
|
||||
month,
|
||||
})
|
||||
if (data && Array.isArray(data)) {
|
||||
const dates = data.map((record: any) => record.signinDate)
|
||||
setSignedDates(dates)
|
||||
setTodaySigned(dates.includes(todayStr))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载签到记录失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignin = async () => {
|
||||
if (todaySigned) {
|
||||
Taro.showToast({ title: '今日已签到', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (loading) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await addShopSigninRecord({
|
||||
userId: (user as any)?.userId,
|
||||
signinDate: todayStr,
|
||||
})
|
||||
Taro.showToast({ title: '签到成功 +5积分', icon: 'success' })
|
||||
setTodaySigned(true)
|
||||
setSignedDates(prev => [...prev, todayStr])
|
||||
} catch (err: any) {
|
||||
console.error('签到失败', err)
|
||||
Taro.showToast({ title: err.message || '签到失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成本周日期列表(用于UI显示)
|
||||
const getWeekDates = () => {
|
||||
const dates = []
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(today)
|
||||
d.setDate(d.getDate() - i)
|
||||
dates.push(d)
|
||||
}
|
||||
return dates
|
||||
}
|
||||
const weekDates = getWeekDates()
|
||||
|
||||
const getDayOfWeek = (date: Date) => {
|
||||
const days = ['日', '一', '二', '三', '四', '五', '六']
|
||||
return days[date.getDay()]
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 p-4'>
|
||||
{/* 签到卡片 */}
|
||||
<View className='bg-white rounded-xl p-6 text-center'>
|
||||
<Text className='text-lg font-bold text-gray-800 block mb-1'>每日签到</Text>
|
||||
<Text className='text-sm text-gray-500 block mb-4'>连续签到获得更多积分</Text>
|
||||
|
||||
{/* 本周签到 */}
|
||||
<View className='grid grid-cols-7 gap-1 mb-4'>
|
||||
{weekDates.map((date, idx) => {
|
||||
const dateStr = date.toISOString().split('T')[0]
|
||||
const dayOfWeek = getDayOfWeek(date)
|
||||
const isSigned = signedDates.includes(dateStr)
|
||||
const isToday = dateStr === todayStr
|
||||
return (
|
||||
<View key={idx} className='flex flex-col items-center py-2'>
|
||||
<Text className='text-xs text-gray-400 mb-1'>周{dayOfWeek}</Text>
|
||||
<View
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
isSigned
|
||||
? 'bg-green-500'
|
||||
: isToday
|
||||
? 'bg-green-50 border border-green-500'
|
||||
: 'bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Text className={`text-xs ${isSigned ? 'text-white' : isToday ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View
|
||||
className='py-2 rounded-full'
|
||||
style={{ backgroundColor: todaySigned ? '#ccc' : '#0e932e' }}
|
||||
onClick={handleSignin}
|
||||
>
|
||||
<Text className='text-white font-medium text-sm'>
|
||||
{loading ? '签到中...' : todaySigned ? '已签到' : '立即签到'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 签到规则 */}
|
||||
<View className='bg-white rounded-xl p-4 mt-4'>
|
||||
<Text className='text-sm font-medium text-gray-800 mb-2'>签到规则</Text>
|
||||
<View className='text-xs text-gray-500 leading-6'>
|
||||
<Text className='block'>1. 每日签到可获得 5 积分</Text>
|
||||
<Text className='block'>2. 连续签到 7 天额外奖励 20 积分</Text>
|
||||
<Text className='block'>3. 连续签到 30 天额外奖励 100 积分</Text>
|
||||
<Text className='block'>4. 中断签到后连续天数重新计算</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default SigninPage
|
||||
Reference in New Issue
Block a user