forked from gxwebsoft/mp-10550
新增:优惠券、积分明细
This commit is contained in:
113
src/api/shop/shopGoodsReview/model/index.ts
Normal file
113
src/api/shop/shopGoodsReview/model/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
102
src/api/user/balance-log/index.ts
Normal file
102
src/api/user/balance-log/index.ts
Normal file
@@ -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));
|
||||
}
|
||||
31
src/api/user/balance-log/model/index.ts
Normal file
31
src/api/user/balance-log/model/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
78
src/api/user/coupon/index.ts
Normal file
78
src/api/user/coupon/index.ts
Normal file
@@ -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));
|
||||
}
|
||||
45
src/api/user/coupon/model/index.ts
Normal file
45
src/api/user/coupon/model/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
73
src/api/user/points/index.ts
Normal file
73
src/api/user/points/index.ts
Normal file
@@ -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));
|
||||
}
|
||||
49
src/api/user/points/model/index.ts
Normal file
49
src/api/user/points/model/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -36,7 +36,10 @@ export default defineAppConfig({
|
||||
"address/add",
|
||||
"address/wxAddress",
|
||||
"help/index",
|
||||
"about/index"
|
||||
"about/index",
|
||||
"wallet/wallet",
|
||||
"coupon/coupon",
|
||||
"points/points"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,11 +6,15 @@ import {useEffect, useState} from "react";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import navTo from "@/utils/common";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {getUserCouponCount} from "@/api/user/coupon";
|
||||
import {getUserPointsStats} from "@/api/user/points";
|
||||
|
||||
function UserCard() {
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [roleName, setRoleName] = useState<string>('注册用户')
|
||||
const [couponCount, setCouponCount] = useState(0)
|
||||
const [pointsCount, setPointsCount] = useState(0)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,6 +34,26 @@ function UserCard() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUserStats = (userId: number) => {
|
||||
// 加载优惠券数量
|
||||
getUserCouponCount(userId)
|
||||
.then((res: any) => {
|
||||
setCouponCount(res.unused || 0)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Coupon count error:', error)
|
||||
})
|
||||
|
||||
// 加载积分数量
|
||||
getUserPointsStats(userId)
|
||||
.then((res: any) => {
|
||||
setPointsCount(res.currentPoints || 0)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Points stats error:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
@@ -44,6 +68,12 @@ function UserCard() {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
|
||||
// 加载用户统计数据
|
||||
if (data.userId) {
|
||||
loadUserStats(data.userId)
|
||||
}
|
||||
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
@@ -186,17 +216,17 @@ function UserCard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex justify-around mt-5'}>
|
||||
<div className={'item flex justify-center flex-col items-center'}>
|
||||
<div className={'item flex justify-center flex-col items-center'} onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||
<span className={'text-sm text-gray-500'}>余额</span>
|
||||
<span className={'text-xl'}>¥ {userInfo?.balance}</span>
|
||||
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'}>
|
||||
<div className={'item flex justify-center flex-col items-center'} onClick={() => navTo('/user/coupon/coupon', true)}>
|
||||
<span className={'text-sm text-gray-500'}>优惠券</span>
|
||||
<span className={'text-xl'}>0</span>
|
||||
<span className={'text-xl'}>{couponCount}</span>
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'}>
|
||||
<div className={'item flex justify-center flex-col items-center'} onClick={() => navTo('/user/points/points', true)}>
|
||||
<span className={'text-sm text-gray-500'}>积分</span>
|
||||
<span className={'text-xl'}>100</span>
|
||||
<span className={'text-xl'}>{pointsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
4
src/user/coupon/coupon.ts
Normal file
4
src/user/coupon/coupon.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的优惠券',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
213
src/user/coupon/coupon.tsx
Normal file
213
src/user/coupon/coupon.tsx
Normal file
@@ -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;
|
||||
4
src/user/points/points.config.ts
Normal file
4
src/user/points/points.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的积分',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
199
src/user/points/points.tsx
Normal file
199
src/user/points/points.tsx
Normal file
@@ -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;
|
||||
4
src/user/wallet/wallet.config.ts
Normal file
4
src/user/wallet/wallet.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '余额明细',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
0
src/user/wallet/wallet.scss
Normal file
0
src/user/wallet/wallet.scss
Normal file
134
src/user/wallet/wallet.tsx
Normal file
134
src/user/wallet/wallet.tsx
Normal file
@@ -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;
|
||||
@@ -108,6 +108,20 @@ export function truncateText(text: string, maxLength: number = 30): string {
|
||||
return text.substring(0, maxLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param amount
|
||||
* @param currency
|
||||
*/
|
||||
export function formatCurrency(amount: number, currency: string = 'CNY'): string {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单标题
|
||||
* @param goodsNames 商品名称数组
|
||||
|
||||
@@ -4,8 +4,8 @@ import {User} from "@/api/system/user/model";
|
||||
// 模版套餐ID - 请根据实际情况修改
|
||||
export const TEMPLATE_ID = '10550';
|
||||
// 服务接口 - 请根据实际情况修改
|
||||
export const SERVER_API_URL = 'https://server.websoft.top/api';
|
||||
// export const SERVER_API_URL = 'http://127.0.0.1:8000/api';
|
||||
// export const SERVER_API_URL = 'https://server.websoft.top/api';
|
||||
export const SERVER_API_URL = 'http://127.0.0.1:8000/api';
|
||||
/**
|
||||
* 保存用户信息到本地存储
|
||||
* @param token
|
||||
|
||||
Reference in New Issue
Block a user