18 changed files with 1105 additions and 9 deletions
@ -0,0 +1,113 @@ |
|||
import type { PageParam } from '@/api/index'; |
|||
|
|||
/** |
|||
* 商品评价 |
|||
*/ |
|||
export interface ShopGoodsReview { |
|||
// 评价ID
|
|||
reviewId?: number; |
|||
// 商品ID
|
|||
goodsId?: number; |
|||
// 订单ID
|
|||
orderId?: number; |
|||
// 用户ID
|
|||
userId?: number; |
|||
// 用户昵称
|
|||
nickname?: string; |
|||
// 用户头像
|
|||
avatar?: string; |
|||
// 评价内容
|
|||
content?: string; |
|||
// 评分 1-5星
|
|||
rating?: number; |
|||
// 评价图片,JSON数组格式
|
|||
images?: string; |
|||
// 是否匿名评价
|
|||
isAnonymous?: boolean; |
|||
// 商家回复
|
|||
reply?: string; |
|||
// 商家回复时间
|
|||
replyTime?: string; |
|||
// 评价状态 0待审核 1已通过 2已拒绝
|
|||
status?: number; |
|||
// 是否置顶
|
|||
isTop?: boolean; |
|||
// 点赞数
|
|||
likeCount?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 更新时间
|
|||
updateTime?: string; |
|||
// 商品信息
|
|||
goodsName?: string; |
|||
goodsImage?: string; |
|||
goodsPrice?: string; |
|||
// SKU信息
|
|||
skuId?: number; |
|||
specInfo?: string; |
|||
} |
|||
|
|||
/** |
|||
* 评价统计 |
|||
*/ |
|||
export interface ReviewStats { |
|||
// 总评价数
|
|||
totalCount: number; |
|||
// 好评数
|
|||
goodCount: number; |
|||
// 中评数
|
|||
mediumCount: number; |
|||
// 差评数
|
|||
badCount: number; |
|||
// 好评率
|
|||
goodRate: number; |
|||
// 平均评分
|
|||
avgRating: number; |
|||
// 各星级统计
|
|||
ratingStats: { |
|||
[key: number]: number; // 星级 -> 数量
|
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* 评价查询参数 |
|||
*/ |
|||
export interface ShopGoodsReviewParam extends PageParam { |
|||
// 商品ID
|
|||
goodsId?: number; |
|||
// 用户ID
|
|||
userId?: number; |
|||
// 订单ID
|
|||
orderId?: number; |
|||
// 评分筛选
|
|||
rating?: number; |
|||
// 状态筛选
|
|||
status?: number; |
|||
// 是否有图片
|
|||
hasImages?: boolean; |
|||
// 排序方式 time:时间 rating:评分 like:点赞数
|
|||
sortBy?: string; |
|||
// 排序方向 asc:升序 desc:降序
|
|||
sortOrder?: string; |
|||
} |
|||
|
|||
/** |
|||
* 提交评价请求 |
|||
*/ |
|||
export interface SubmitReviewRequest { |
|||
// 商品ID
|
|||
goodsId: number; |
|||
// 订单ID
|
|||
orderId: number; |
|||
// 评价内容
|
|||
content: string; |
|||
// 评分
|
|||
rating: number; |
|||
// 评价图片
|
|||
images?: string[]; |
|||
// 是否匿名
|
|||
isAnonymous?: boolean; |
|||
// SKU信息
|
|||
skuId?: number; |
|||
specInfo?: string; |
|||
} |
@ -0,0 +1,102 @@ |
|||
import request from '@/utils/request'; |
|||
import type { ApiResult, PageResult } from '@/api/index'; |
|||
import type { UserBalanceLog, UserBalanceLogParam } from './model'; |
|||
import {SERVER_API_URL} from "@/utils/server"; |
|||
|
|||
/** |
|||
* 分页查询余额明细 |
|||
*/ |
|||
export async function pageUserBalanceLog(params: UserBalanceLogParam) { |
|||
const res = await request.get<ApiResult<PageResult<UserBalanceLog>>>( |
|||
SERVER_API_URL + '/sys/user-balance-log/page', |
|||
params |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 查询余额明细列表 |
|||
*/ |
|||
export async function listUserBalanceLog(params?: UserBalanceLogParam) { |
|||
const res = await request.get<ApiResult<UserBalanceLog[]>>( |
|||
SERVER_API_URL + '/sys/user-balance-log', |
|||
params |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 根据id查询余额明细 |
|||
*/ |
|||
export async function getUserBalanceLog(id: number) { |
|||
const res = await request.get<ApiResult<UserBalanceLog>>( |
|||
SERVER_API_URL + '/sys/user-balance-log/' + id |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 添加余额明细 |
|||
*/ |
|||
export async function addUserBalanceLog(data: UserBalanceLog) { |
|||
const res = await request.post<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/sys/user-balance-log', |
|||
data |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 修改余额明细 |
|||
*/ |
|||
export async function updateUserBalanceLog(data: UserBalanceLog) { |
|||
const res = await request.put<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/sys/user-balance-log', |
|||
data |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 删除余额明细 |
|||
*/ |
|||
export async function removeUserBalanceLog(id?: number) { |
|||
const res = await request.del<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/sys/user-balance-log/' + id |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 批量删除余额明细 |
|||
*/ |
|||
export async function removeUserBalanceLogs(data: (number | undefined)[]) { |
|||
const res = await request.del<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/sys/user-balance-log/batch', |
|||
{ |
|||
data |
|||
} |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
@ -0,0 +1,31 @@ |
|||
import type { PageParam } from '@/api/index'; |
|||
|
|||
/** |
|||
* 余额明细 |
|||
*/ |
|||
export interface UserBalanceLog { |
|||
logId?: number; |
|||
userId?: number; |
|||
scene?: number; |
|||
money?: string; |
|||
describe?: string; |
|||
remark?: string; |
|||
sortNumber?: number; |
|||
comments?: string; |
|||
status?: number; |
|||
deleted?: number; |
|||
tenantId?: number; |
|||
createTime?: string; |
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 用户搜索条件 |
|||
*/ |
|||
export interface UserBalanceLogParam extends PageParam { |
|||
logId?: number; |
|||
userId?: number; |
|||
scene?: number; |
|||
createTimeStart?: string; |
|||
createTimeEnd?: string; |
|||
} |
@ -0,0 +1,78 @@ |
|||
import request from '@/utils/request'; |
|||
import type { ApiResult, PageResult } from '@/api/index'; |
|||
import type { UserCoupon, UserCouponParam } from './model'; |
|||
import {SERVER_API_URL} from "@/utils/server"; |
|||
|
|||
/** |
|||
* 分页查询用户优惠券 |
|||
*/ |
|||
export async function pageUserCoupon(params: UserCouponParam) { |
|||
const res = await request.get<ApiResult<PageResult<UserCoupon>>>( |
|||
SERVER_API_URL + '/sys/user-coupon/page', |
|||
params |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 查询用户优惠券列表 |
|||
*/ |
|||
export async function listUserCoupon(params?: UserCouponParam) { |
|||
const res = await request.get<ApiResult<UserCoupon[]>>( |
|||
SERVER_API_URL + '/sys/user-coupon', |
|||
params |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 获取用户优惠券统计 |
|||
*/ |
|||
export async function getUserCouponCount(userId: number) { |
|||
const res = await request.get<ApiResult<{ |
|||
total: number; |
|||
unused: number; |
|||
used: number; |
|||
expired: number; |
|||
}>>( |
|||
SERVER_API_URL + '/sys/user-coupon/count', |
|||
{ userId } |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 根据id查询用户优惠券 |
|||
*/ |
|||
export async function getUserCoupon(id: number) { |
|||
const res = await request.get<ApiResult<UserCoupon>>( |
|||
SERVER_API_URL + '/sys/user-coupon/' + id |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 使用优惠券 |
|||
*/ |
|||
export async function useCoupon(couponId: number, orderId: number) { |
|||
const res = await request.put<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/sys/user-coupon/use', |
|||
{ couponId, orderId } |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
@ -0,0 +1,45 @@ |
|||
import type { PageParam } from '@/api/index'; |
|||
|
|||
/** |
|||
* 用户优惠券 |
|||
*/ |
|||
export interface UserCoupon { |
|||
// 优惠券ID
|
|||
couponId?: number; |
|||
// 用户ID
|
|||
userId?: number; |
|||
// 优惠券名称
|
|||
name?: string; |
|||
// 优惠券类型 1-满减券 2-折扣券 3-免费券
|
|||
type?: number; |
|||
// 优惠券金额/折扣
|
|||
value?: string; |
|||
// 使用门槛金额
|
|||
minAmount?: string; |
|||
// 有效期开始时间
|
|||
startTime?: string; |
|||
// 有效期结束时间
|
|||
endTime?: string; |
|||
// 使用状态 0-未使用 1-已使用 2-已过期
|
|||
status?: number; |
|||
// 使用时间
|
|||
useTime?: string; |
|||
// 关联订单ID
|
|||
orderId?: number; |
|||
// 备注
|
|||
comments?: string; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 更新时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 用户优惠券搜索条件 |
|||
*/ |
|||
export interface UserCouponParam extends PageParam { |
|||
userId?: number; |
|||
type?: number; |
|||
status?: number; |
|||
name?: string; |
|||
} |
@ -0,0 +1,73 @@ |
|||
import request from '@/utils/request'; |
|||
import type { ApiResult, PageResult } from '@/api/index'; |
|||
import type { UserPointsLog, UserPointsLogParam, UserPointsStats } from './model'; |
|||
import {SERVER_API_URL} from "@/utils/server"; |
|||
|
|||
/** |
|||
* 分页查询用户积分记录 |
|||
*/ |
|||
export async function pageUserPointsLog(params: UserPointsLogParam) { |
|||
const res = await request.get<ApiResult<PageResult<UserPointsLog>>>( |
|||
SERVER_API_URL + '/sys/user-points-log/page', |
|||
params |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 查询用户积分记录列表 |
|||
*/ |
|||
export async function listUserPointsLog(params?: UserPointsLogParam) { |
|||
const res = await request.get<ApiResult<UserPointsLog[]>>( |
|||
SERVER_API_URL + '/sys/user-points-log', |
|||
params |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 获取用户积分统计 |
|||
*/ |
|||
export async function getUserPointsStats(userId: number) { |
|||
const res = await request.get<ApiResult<UserPointsStats>>( |
|||
SERVER_API_URL + '/sys/user-points-log/stats', |
|||
{ userId } |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 根据id查询积分记录 |
|||
*/ |
|||
export async function getUserPointsLog(id: number) { |
|||
const res = await request.get<ApiResult<UserPointsLog>>( |
|||
SERVER_API_URL + '/sys/user-points-log/' + id |
|||
); |
|||
if (res.code === 0 && res.data) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 使用积分 |
|||
*/ |
|||
export async function usePoints(userId: number, points: number, reason: string, orderId?: number) { |
|||
const res = await request.post<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/sys/user-points-log/use', |
|||
{ userId, points, reason, orderId } |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
@ -0,0 +1,49 @@ |
|||
import type { PageParam } from '@/api/index'; |
|||
|
|||
/** |
|||
* 用户积分记录 |
|||
*/ |
|||
export interface UserPointsLog { |
|||
// 积分记录ID
|
|||
logId?: number; |
|||
// 用户ID
|
|||
userId?: number; |
|||
// 积分变动类型 1-获得 2-消费 3-过期 4-管理员调整
|
|||
type?: number; |
|||
// 积分变动数量
|
|||
points?: number; |
|||
// 变动原因
|
|||
reason?: string; |
|||
// 关联订单ID
|
|||
orderId?: number; |
|||
// 备注
|
|||
comments?: string; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 更新时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 用户积分统计 |
|||
*/ |
|||
export interface UserPointsStats { |
|||
// 当前积分
|
|||
currentPoints?: number; |
|||
// 累计获得积分
|
|||
totalEarned?: number; |
|||
// 累计消费积分
|
|||
totalUsed?: number; |
|||
// 即将过期积分
|
|||
expiringSoon?: number; |
|||
} |
|||
|
|||
/** |
|||
* 用户积分搜索条件 |
|||
*/ |
|||
export interface UserPointsLogParam extends PageParam { |
|||
userId?: number; |
|||
type?: number; |
|||
startTime?: string; |
|||
endTime?: string; |
|||
} |
@ -0,0 +1,4 @@ |
|||
export default definePageConfig({ |
|||
navigationBarTitleText: '我的优惠券', |
|||
navigationBarTextStyle: 'black' |
|||
}) |
@ -0,0 +1,213 @@ |
|||
import {useEffect, useState} from "react"; |
|||
import Taro from '@tarojs/taro' |
|||
import {Button, Cell, Space, Empty, ConfigProvider, Tabs, TabPane, Tag} from '@nutui/nutui-react-taro' |
|||
import {View} from '@tarojs/components' |
|||
import {pageUserCoupon, getUserCouponCount} from "@/api/user/coupon"; |
|||
import {UserCoupon as UserCouponType} from "@/api/user/coupon/model"; |
|||
|
|||
const UserCoupon = () => { |
|||
const [list, setList] = useState<UserCouponType[]>([]) |
|||
const [loading, setLoading] = useState(false) |
|||
const [activeTab, setActiveTab] = useState('0') |
|||
const [couponCount, setCouponCount] = useState({ |
|||
total: 0, |
|||
unused: 0, |
|||
used: 0, |
|||
expired: 0 |
|||
}) |
|||
|
|||
const tabs = [ |
|||
{ key: '0', title: '全部', status: undefined }, |
|||
{ key: '1', title: '未使用', status: 0 }, |
|||
{ key: '2', title: '已使用', status: 1 }, |
|||
{ key: '3', title: '已过期', status: 2 } |
|||
] |
|||
|
|||
const reload = (status?: number) => { |
|||
setLoading(true) |
|||
const userId = Taro.getStorageSync('UserId') |
|||
|
|||
console.log('Loading coupons for userId:', userId, 'status:', status) |
|||
|
|||
if (!userId) { |
|||
console.warn('No userId found in storage') |
|||
Taro.showToast({ |
|||
title: '请先登录', |
|||
icon: 'error' |
|||
}); |
|||
setLoading(false) |
|||
return |
|||
} |
|||
|
|||
pageUserCoupon({ |
|||
userId: parseInt(userId), |
|||
status: status, |
|||
page: 1, |
|||
limit: 20 |
|||
}) |
|||
.then((res: any) => { |
|||
console.log('Coupon response:', res) |
|||
setList(res?.list || []) |
|||
}) |
|||
.catch((error: any) => { |
|||
console.error('Coupon error:', error) |
|||
Taro.showToast({ |
|||
title: error?.message || '获取失败', |
|||
icon: 'error' |
|||
}); |
|||
}) |
|||
.finally(() => { |
|||
setLoading(false) |
|||
}) |
|||
} |
|||
|
|||
const loadCouponCount = () => { |
|||
const userId = Taro.getStorageSync('UserId') |
|||
if (!userId) return |
|||
|
|||
getUserCouponCount(parseInt(userId)) |
|||
.then((res: any) => { |
|||
setCouponCount(res) |
|||
}) |
|||
.catch((error: any) => { |
|||
console.error('Coupon count error:', error) |
|||
}) |
|||
} |
|||
|
|||
useEffect(() => { |
|||
reload() |
|||
loadCouponCount() |
|||
}, []); |
|||
|
|||
const onTabChange = (index: string) => { |
|||
setActiveTab(index) |
|||
const tab = tabs.find(t => t.key === index) |
|||
reload(tab?.status) |
|||
} |
|||
|
|||
const getCouponTypeText = (type?: number) => { |
|||
switch (type) { |
|||
case 1: return '满减券' |
|||
case 2: return '折扣券' |
|||
case 3: return '免费券' |
|||
default: return '优惠券' |
|||
} |
|||
} |
|||
|
|||
const getCouponStatusText = (status?: number) => { |
|||
switch (status) { |
|||
case 0: return '未使用' |
|||
case 1: return '已使用' |
|||
case 2: return '已过期' |
|||
default: return '未知' |
|||
} |
|||
} |
|||
|
|||
const getCouponStatusColor = (status?: number) => { |
|||
switch (status) { |
|||
case 0: return 'success' |
|||
case 1: return 'default' |
|||
case 2: return 'danger' |
|||
default: return 'default' |
|||
} |
|||
} |
|||
|
|||
const formatCouponValue = (type?: number, value?: string) => { |
|||
if (!value) return '0' |
|||
switch (type) { |
|||
case 1: return `¥${value}` |
|||
case 2: return `${parseFloat(value) * 10}折` |
|||
case 3: return '免费' |
|||
default: return value |
|||
} |
|||
} |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<ConfigProvider> |
|||
<div className={'h-full flex flex-col justify-center items-center'} style={{ |
|||
height: 'calc(100vh - 300px)', |
|||
}}> |
|||
<div>加载中...</div> |
|||
</div> |
|||
</ConfigProvider> |
|||
) |
|||
} |
|||
|
|||
if (list.length == 0) { |
|||
return ( |
|||
<ConfigProvider> |
|||
<div className={'h-full flex flex-col justify-center items-center'} style={{ |
|||
height: 'calc(100vh - 300px)', |
|||
}}> |
|||
<Empty |
|||
style={{ |
|||
backgroundColor: 'transparent' |
|||
}} |
|||
description="您还没有优惠券" |
|||
/> |
|||
<Space> |
|||
<Button onClick={() => reload()}>刷新</Button> |
|||
</Space> |
|||
</div> |
|||
</ConfigProvider> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<ConfigProvider> |
|||
<View> |
|||
<Tabs value={activeTab} onChange={onTabChange}> |
|||
{tabs.map(tab => ( |
|||
<TabPane key={tab.key} title={tab.title}> |
|||
<View className="p-4"> |
|||
{list.map((item, index) => ( |
|||
<Cell.Group key={index} className="mb-4"> |
|||
<Cell className="coupon-item p-4"> |
|||
<View className="flex justify-between items-center"> |
|||
<View className="flex-1"> |
|||
<View className="flex items-center mb-2"> |
|||
<View className="coupon-value text-2xl font-bold text-red-500 mr-3"> |
|||
{formatCouponValue(item.type, item.value)} |
|||
</View> |
|||
<View className="flex flex-col"> |
|||
<View className="text-base font-medium text-gray-800"> |
|||
{item.name || getCouponTypeText(item.type)} |
|||
</View> |
|||
{item.minAmount && parseFloat(item.minAmount) > 0 && ( |
|||
<View className="text-sm text-gray-500"> |
|||
满¥{item.minAmount}可用 |
|||
</View> |
|||
)} |
|||
</View> |
|||
</View> |
|||
|
|||
<View className="flex justify-between items-center text-xs text-gray-400"> |
|||
<View> |
|||
有效期: {item.startTime ? new Date(item.startTime).toLocaleDateString() : ''} - {item.endTime ? new Date(item.endTime).toLocaleDateString() : ''} |
|||
</View> |
|||
<Tag type={getCouponStatusColor(item.status)} size="small"> |
|||
{getCouponStatusText(item.status)} |
|||
</Tag> |
|||
</View> |
|||
|
|||
{item.comments && ( |
|||
<View className="text-xs text-gray-500 mt-2 p-2 bg-gray-50 rounded"> |
|||
{item.comments} |
|||
</View> |
|||
)} |
|||
</View> |
|||
</View> |
|||
</Cell> |
|||
</Cell.Group> |
|||
))} |
|||
</View> |
|||
</TabPane> |
|||
))} |
|||
</Tabs> |
|||
</View> |
|||
</ConfigProvider> |
|||
); |
|||
}; |
|||
|
|||
export default UserCoupon; |
@ -0,0 +1,4 @@ |
|||
export default definePageConfig({ |
|||
navigationBarTitleText: '我的积分', |
|||
navigationBarTextStyle: 'black' |
|||
}) |
@ -0,0 +1,199 @@ |
|||
import {useEffect, useState} from "react"; |
|||
import Taro from '@tarojs/taro' |
|||
import {Button, Cell, Space, Empty, ConfigProvider, Card} from '@nutui/nutui-react-taro' |
|||
import {View} from '@tarojs/components' |
|||
import {pageUserPointsLog, getUserPointsStats} from "@/api/user/points"; |
|||
import {UserPointsLog as UserPointsLogType, UserPointsStats} from "@/api/user/points/model"; |
|||
|
|||
const UserPoints = () => { |
|||
const [list, setList] = useState<UserPointsLogType[]>([]) |
|||
const [loading, setLoading] = useState(false) |
|||
const [stats, setStats] = useState<UserPointsStats>({}) |
|||
|
|||
const reload = () => { |
|||
setLoading(true) |
|||
const userId = Taro.getStorageSync('UserId') |
|||
|
|||
console.log('Loading points log for userId:', userId) |
|||
|
|||
if (!userId) { |
|||
console.warn('No userId found in storage') |
|||
Taro.showToast({ |
|||
title: '请先登录', |
|||
icon: 'error' |
|||
}); |
|||
setLoading(false) |
|||
return |
|||
} |
|||
|
|||
pageUserPointsLog({ |
|||
userId: parseInt(userId), |
|||
page: 1, |
|||
limit: 20 |
|||
}) |
|||
.then((res: any) => { |
|||
console.log('Points log response:', res) |
|||
setList(res?.list || []) |
|||
}) |
|||
.catch((error: any) => { |
|||
console.error('Points log error:', error) |
|||
Taro.showToast({ |
|||
title: error?.message || '获取失败', |
|||
icon: 'error' |
|||
}); |
|||
}) |
|||
.finally(() => { |
|||
setLoading(false) |
|||
}) |
|||
} |
|||
|
|||
const loadPointsStats = () => { |
|||
const userId = Taro.getStorageSync('UserId') |
|||
if (!userId) return |
|||
|
|||
getUserPointsStats(parseInt(userId)) |
|||
.then((res: any) => { |
|||
setStats(res) |
|||
}) |
|||
.catch((error: any) => { |
|||
console.error('Points stats error:', error) |
|||
}) |
|||
} |
|||
|
|||
useEffect(() => { |
|||
reload() |
|||
loadPointsStats() |
|||
}, []); |
|||
|
|||
const getPointsTypeText = (type?: number) => { |
|||
switch (type) { |
|||
case 1: return '获得积分' |
|||
case 2: return '消费积分' |
|||
case 3: return '积分过期' |
|||
case 4: return '管理员调整' |
|||
default: return '积分变动' |
|||
} |
|||
} |
|||
|
|||
const getPointsTypeColor = (type?: number) => { |
|||
switch (type) { |
|||
case 1: return 'text-green-500' |
|||
case 2: return 'text-red-500' |
|||
case 3: return 'text-gray-500' |
|||
case 4: return 'text-blue-500' |
|||
default: return 'text-gray-500' |
|||
} |
|||
} |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<ConfigProvider> |
|||
<div className={'h-full flex flex-col justify-center items-center'} style={{ |
|||
height: 'calc(100vh - 300px)', |
|||
}}> |
|||
<div>加载中...</div> |
|||
</div> |
|||
</ConfigProvider> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<ConfigProvider> |
|||
<View className="bg-gray-50 min-h-screen"> |
|||
{/* 积分统计卡片 */} |
|||
<View className="p-4"> |
|||
<Card className="points-stats-card"> |
|||
<View className="text-center py-4"> |
|||
<View className="text-3xl font-bold text-orange-500 mb-2"> |
|||
{stats.currentPoints || 0} |
|||
</View> |
|||
<View className="text-sm text-gray-500 mb-4">当前积分</View> |
|||
|
|||
<View className="flex justify-around text-center"> |
|||
<View> |
|||
<View className="text-lg font-medium text-gray-800"> |
|||
{stats.totalEarned || 0} |
|||
</View> |
|||
<View className="text-xs text-gray-500">累计获得</View> |
|||
</View> |
|||
<View> |
|||
<View className="text-lg font-medium text-gray-800"> |
|||
{stats.totalUsed || 0} |
|||
</View> |
|||
<View className="text-xs text-gray-500">累计消费</View> |
|||
</View> |
|||
<View> |
|||
<View className="text-lg font-medium text-gray-800"> |
|||
{stats.expiringSoon || 0} |
|||
</View> |
|||
<View className="text-xs text-gray-500">即将过期</View> |
|||
</View> |
|||
</View> |
|||
</View> |
|||
</Card> |
|||
</View> |
|||
|
|||
{/* 积分记录 */} |
|||
<View className="px-4"> |
|||
<View className="text-base font-medium text-gray-800 mb-3">积分明细</View> |
|||
|
|||
{list.length === 0 ? ( |
|||
<div className={'h-full flex flex-col justify-center items-center'} style={{ |
|||
height: 'calc(100vh - 400px)', |
|||
}}> |
|||
<Empty |
|||
style={{ |
|||
backgroundColor: 'transparent' |
|||
}} |
|||
description="您还没有积分记录" |
|||
/> |
|||
<Space> |
|||
<Button onClick={() => reload()}>刷新</Button> |
|||
</Space> |
|||
</div> |
|||
) : ( |
|||
list.map((item, index) => ( |
|||
<Cell.Group key={index} className="mb-3"> |
|||
<Cell className="flex flex-col gap-2 p-4"> |
|||
<View className="flex justify-between items-start"> |
|||
<View className="flex-1"> |
|||
<View className="font-medium text-base text-gray-800 mb-1"> |
|||
{getPointsTypeText(item.type)} |
|||
</View> |
|||
<View className="text-sm text-gray-500"> |
|||
{item.reason || '无备注'} |
|||
</View> |
|||
</View> |
|||
<View className={`text-lg font-bold ${getPointsTypeColor(item.type)}`}> |
|||
{item.type === 1 ? '+' : item.type === 2 ? '-' : ''} |
|||
{item.points || 0} |
|||
</View> |
|||
</View> |
|||
|
|||
<View className="flex justify-between items-center text-xs text-gray-400 mt-2"> |
|||
<View> |
|||
{item.createTime ? new Date(item.createTime).toLocaleString() : ''} |
|||
</View> |
|||
{item.orderId && ( |
|||
<View> |
|||
订单: {item.orderId} |
|||
</View> |
|||
)} |
|||
</View> |
|||
|
|||
{item.comments && ( |
|||
<View className="text-xs text-gray-500 mt-1 p-2 bg-gray-50 rounded"> |
|||
备注: {item.comments} |
|||
</View> |
|||
)} |
|||
</Cell> |
|||
</Cell.Group> |
|||
)) |
|||
)} |
|||
</View> |
|||
</View> |
|||
</ConfigProvider> |
|||
); |
|||
}; |
|||
|
|||
export default UserPoints; |
@ -0,0 +1,4 @@ |
|||
export default definePageConfig({ |
|||
navigationBarTitleText: '余额明细', |
|||
navigationBarTextStyle: 'black' |
|||
}) |
@ -0,0 +1,134 @@ |
|||
import {useEffect, useState} from "react"; |
|||
import Taro from '@tarojs/taro' |
|||
import {Button, Cell, Space, Empty, ConfigProvider, InfiniteLoading} from '@nutui/nutui-react-taro' |
|||
import {View, ScrollView} from '@tarojs/components' |
|||
import {pageUserBalanceLog} from "@/api/user/balance-log"; |
|||
import {UserBalanceLog} from "@/api/user/balance-log/model"; |
|||
import {formatCurrency} from "@/utils/common"; |
|||
|
|||
const Wallet = () => { |
|||
const [list, setList] = useState<UserBalanceLog[]>([]) |
|||
const [loading, setLoading] = useState(false) |
|||
const [loadingMore, setLoadingMore] = useState(false) |
|||
const [refreshing, setRefreshing] = useState(false) |
|||
const [hasMore, setHasMore] = useState(true) |
|||
const [currentPage, setCurrentPage] = useState(1) |
|||
const [total, setTotal] = useState(0) |
|||
const pageSize = 20 |
|||
|
|||
const reload = () => { |
|||
setLoading(true) |
|||
const userId = Taro.getStorageSync('UserId') |
|||
|
|||
console.log('Loading balance log for userId:', userId) |
|||
|
|||
if (!userId) { |
|||
console.warn('No userId found in storage') |
|||
Taro.showToast({ |
|||
title: '请先登录', |
|||
icon: 'error' |
|||
}); |
|||
setLoading(false) |
|||
return |
|||
} |
|||
|
|||
pageUserBalanceLog({ |
|||
userId: parseInt(userId), |
|||
page: 1, |
|||
limit: 20 |
|||
}) |
|||
.then((res: any) => { |
|||
console.log('Balance log response:', res) |
|||
setList(res?.list || []) |
|||
}) |
|||
.catch((error: any) => { |
|||
console.error('Balance log error:', error) |
|||
Taro.showToast({ |
|||
title: error?.message || '获取失败', |
|||
icon: 'error' |
|||
}); |
|||
}) |
|||
.finally(() => { |
|||
setLoading(false) |
|||
}) |
|||
} |
|||
|
|||
useEffect(() => { |
|||
reload() |
|||
}, []); |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<ConfigProvider> |
|||
<div className={'h-full flex flex-col justify-center items-center'} style={{ |
|||
height: 'calc(100vh - 300px)', |
|||
}}> |
|||
<div>加载中...</div> |
|||
</div> |
|||
</ConfigProvider> |
|||
) |
|||
} |
|||
|
|||
if (list.length == 0) { |
|||
return ( |
|||
<ConfigProvider> |
|||
<div className={'h-full flex flex-col justify-center items-center'} style={{ |
|||
height: 'calc(100vh - 300px)', |
|||
}}> |
|||
<Empty |
|||
style={{ |
|||
backgroundColor: 'transparent' |
|||
}} |
|||
description="您还没有消费记录" |
|||
/> |
|||
<Space> |
|||
<Button onClick={() => reload()}>刷新</Button> |
|||
</Space> |
|||
</div> |
|||
</ConfigProvider> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<ConfigProvider> |
|||
<View className="p-4"> |
|||
{list.map((item, index) => ( |
|||
<Cell.Group key={index} className="mb-4"> |
|||
<Cell className="flex flex-col gap-2 p-4"> |
|||
<View className="flex justify-between items-start w-full"> |
|||
<View className="flex-1"> |
|||
<View className="font-medium text-base text-gray-800 mb-1"> |
|||
{item.scene === 10 ? '会员充值' : item.scene === 20 ? '用户消费' : item.scene === 30 ? '管理员操作' : '订单退款'} |
|||
</View> |
|||
<View className="text-sm text-gray-500"> |
|||
{item.comments} |
|||
</View> |
|||
</View> |
|||
<View className={`text-lg font-bold ${ |
|||
item.scene === 10 ? 'text-green-500' : '' |
|||
}`}>
|
|||
{item.scene === 10 ? '+' : '-'} |
|||
{formatCurrency(Number(item.money), 'CNY') || '0.00'} |
|||
</View> |
|||
</View> |
|||
|
|||
<View className="flex justify-between w-full items-center text-xs text-gray-400 mt-2"> |
|||
<View> |
|||
{item.createTime} |
|||
</View> |
|||
</View> |
|||
|
|||
{item.remark && ( |
|||
<View className="text-xs text-gray-500 mt-1 p-2 bg-gray-50 rounded"> |
|||
备注: {item.remark} |
|||
</View> |
|||
)} |
|||
</Cell> |
|||
</Cell.Group> |
|||
))} |
|||
</View> |
|||
</ConfigProvider> |
|||
); |
|||
}; |
|||
|
|||
export default Wallet; |
Loading…
Reference in new issue