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