Browse Source

新增:优惠券、积分明细

master
科技小王子 2 weeks ago
parent
commit
815f020c50
  1. 2
      config/env.ts
  2. 65
      src/api/shop/shopCoupon/model/index.ts
  3. 35
      src/api/shop/shopGoodsCoupon/model/index.ts
  4. 1
      src/api/user/balance-log/model/index.ts
  5. 7
      src/cms/category/components/ArticleTabs.tsx
  6. 227
      src/user/coupon/coupon.tsx
  7. 214
      src/user/points/points.tsx
  8. 201
      src/user/wallet/wallet.tsx
  9. 4
      src/utils/server.ts

2
config/env.ts

@ -2,7 +2,7 @@
export const ENV_CONFIG = {
// 开发环境
development: {
API_BASE_URL: 'https://cms-api.websoft.top/api',
API_BASE_URL: 'http://127.0.0.1:9200/api',
APP_NAME: '时里院子市集',
DEBUG: 'true',
},

65
src/api/shop/shopCoupon/model/index.ts

@ -0,0 +1,65 @@
import type { PageParam } from '@/api/index';
/**
*
*/
export interface ShopCoupon {
// id
id?: number;
// 优惠券名称
name?: string;
// 优惠券描述
description?: string;
// 优惠券类型(10满减券 20折扣券 30免费劵)
type?: number;
// 满减券-减免金额
reducePrice?: string;
// 折扣券-折扣率(0-100)
discount?: number;
// 最低消费金额
minPrice?: string;
// 到期类型(10领取后生效 20固定时间)
expireType?: number;
// 领取后生效-有效天数
expireDay?: number;
// 有效期开始时间
startTime?: string;
// 有效期结束时间
endTime?: string;
// 适用范围(10全部商品 20指定商品 30指定分类)
applyRange?: number;
// 适用范围配置(json格式)
applyRangeConfig?: string;
// 是否过期(0未过期 1已过期)
isExpire?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1禁用
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 创建用户ID
userId?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 发放总数量(-1表示无限制)
totalCount?: number;
// 已发放数量
issuedCount?: number;
// 每人限领数量(-1表示无限制)
limitPerUser?: number;
// 是否启用(0禁用 1启用)
enabled?: string;
}
/**
*
*/
export interface ShopCouponParam extends PageParam {
id?: number;
keywords?: string;
}

35
src/api/shop/shopGoodsCoupon/model/index.ts

@ -0,0 +1,35 @@
import type { PageParam } from '@/api/index';
/**
*
*/
export interface ShopGoodsCoupon {
//
id?: number;
// 商品id
goodsId?: number;
// 优惠劵id
issueCouponId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 用户ID
userId?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
*
*/
export interface ShopGoodsCouponParam extends PageParam {
id?: number;
keywords?: string;
}

1
src/api/user/balance-log/model/index.ts

@ -8,6 +8,7 @@ export interface UserBalanceLog {
userId?: number;
scene?: number;
money?: string;
balance?: number;
describe?: string;
remark?: string;
sortNumber?: number;

7
src/cms/category/components/ArticleTabs.tsx

@ -11,7 +11,12 @@ const ArticleTabs = (props: any) => {
const reload = async (value) => {
const {data} = props
pageCmsArticle({categoryId: data[value].navigationId, page: 1, limit: 10}).then((res) => {
pageCmsArticle({
categoryId: data[value].navigationId,
page: 1,
status: 0,
limit: 10
}).then((res) => {
res && setList(res?.list || [])
})
.catch(err => {

227
src/user/coupon/coupon.tsx

@ -1,13 +1,22 @@
import {useEffect, useState} from "react";
import {useState, useEffect, CSSProperties} 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 {Cell, InfiniteLoading, Tabs, TabPane, Tag, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
import {pageUserCoupon, getUserCouponCount} from "@/api/user/coupon";
import {UserCoupon as UserCouponType} from "@/api/user/coupon/model";
import {View} from '@tarojs/components'
const InfiniteUlStyle: CSSProperties = {
height: '100vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
const UserCoupon = () => {
const [list, setList] = useState<UserCouponType[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [activeTab, setActiveTab] = useState('0')
const [couponCount, setCouponCount] = useState({
total: 0,
@ -23,42 +32,43 @@ const UserCoupon = () => {
{ key: '3', title: '已过期', status: 2 }
]
const reload = (status?: number) => {
setLoading(true)
const userId = Taro.getStorageSync('UserId')
useEffect(() => {
reload()
loadCouponCount()
}, [])
console.log('Loading coupons for userId:', userId, 'status:', status)
const loadMore = async () => {
setPage(page + 1)
reload();
}
const reload = () => {
const userId = Taro.getStorageSync('UserId')
if (!userId) {
console.warn('No userId found in storage')
Taro.showToast({
title: '请先登录',
icon: 'error'
});
setLoading(false)
return
}
const tab = tabs.find(t => t.key === activeTab)
pageUserCoupon({
userId: parseInt(userId),
status: status,
page: 1,
limit: 20
status: tab?.status,
page
}).then(res => {
console.log(res)
const newList = res?.list || [];
setList([...list, ...newList])
setHasMore(newList.length > 0)
}).catch(error => {
console.error('Coupon error:', error)
Taro.showToast({
title: error?.message || '获取失败',
icon: 'error'
});
})
.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 = () => {
@ -74,15 +84,15 @@ const UserCoupon = () => {
})
}
useEffect(() => {
reload()
loadCouponCount()
}, []);
const onTabChange = (index: string) => {
setActiveTab(index)
const tab = tabs.find(t => t.key === index)
reload(tab?.status)
setList([]) // 清空列表
setPage(1) // 重置页码
setHasMore(true) // 重置hasMore
// 延迟执行reload,确保状态更新完成
setTimeout(() => {
reload()
}, 0)
}
const getCouponTypeText = (type?: number) => {
@ -122,86 +132,91 @@ const UserCoupon = () => {
}
}
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>
<View className="h-screen">
<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}
<ul style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={loadMore}
onScroll={() => {
console.log('onScroll')
}}
onScrollToUpper={() => {
console.log('onScrollToUpper')
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
<View className="p-4">
{list.length === 0 ? (
<div className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 400px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="您还没有优惠券"
/>
</div>
) : (
list.map((item, index) => (
<Cell.Group key={`${item.couponId}-${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>
</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 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>
)}
</View>
</View>
</Cell>
</Cell.Group>
))}
</View>
</Cell>
</Cell.Group>
))
)}
</View>
</InfiniteLoading>
</ul>
</TabPane>
))}
</Tabs>

214
src/user/points/points.tsx

@ -1,50 +1,59 @@
import {useEffect, useState} from "react";
import {useState, useEffect, CSSProperties} 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 {Cell, InfiniteLoading, Card, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
import {pageUserPointsLog, getUserPointsStats} from "@/api/user/points";
import {UserPointsLog as UserPointsLogType, UserPointsStats} from "@/api/user/points/model";
import {View} from '@tarojs/components'
const InfiniteUlStyle: CSSProperties = {
height: '100vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
const UserPoints = () => {
const [list, setList] = useState<UserPointsLogType[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [stats, setStats] = useState<UserPointsStats>({})
const reload = () => {
setLoading(true)
const userId = Taro.getStorageSync('UserId')
useEffect(() => {
reload()
loadPointsStats()
}, [])
console.log('Loading points log for userId:', userId)
const loadMore = async () => {
setPage(page + 1)
reload();
}
const reload = () => {
const userId = Taro.getStorageSync('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
page
}).then(res => {
console.log(res)
const newList = res?.list || [];
setList([...list, ...newList])
setHasMore(newList.length > 0)
}).catch(error => {
console.error('Points log error:', error)
Taro.showToast({
title: error?.message || '获取失败',
icon: 'error'
});
})
.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 = () => {
@ -60,11 +69,6 @@ const UserPoints = () => {
})
}
useEffect(() => {
reload()
loadPointsStats()
}, []);
const getPointsTypeText = (type?: number) => {
switch (type) {
case 1: return '获得积分'
@ -85,21 +89,9 @@ const UserPoints = () => {
}
}
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="bg-gray-50 h-screen">
{/* 积分统计卡片 */}
<View className="p-4">
<Card className="points-stats-card">
@ -134,62 +126,84 @@ const UserPoints = () => {
</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 className="px-4 flex-1">
<ul style={{...InfiniteUlStyle, height: 'calc(100vh - 200px)'}} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={loadMore}
onScroll={() => {
console.log('onScroll')
}}
onScrollToUpper={() => {
console.log('onScrollToUpper')
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
<View className="p-4">
{list.length === 0 ? (
<div className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 500px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="您还没有积分记录"
/>
</div>
) : (
list.map((item, index) => (
<Cell.Group key={`${item.logId}-${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>
</InfiniteLoading>
</ul>
</View>
</View>
</ConfigProvider>

201
src/user/wallet/wallet.tsx

@ -1,134 +1,105 @@
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 {useState, useEffect, CSSProperties} from 'react'
import {Cell, InfiniteLoading} from '@nutui/nutui-react-taro'
import {pageUserBalanceLog} from "@/api/user/balance-log";
import {UserBalanceLog} from "@/api/user/balance-log/model";
import {formatCurrency} from "@/utils/common";
import {View} from '@tarojs/components'
const InfiniteUlStyle: CSSProperties = {
height: '100vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
const Wallet = () => {
const [list, setList] = useState<UserBalanceLog[]>([])
const [loading, setLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [page, setPage] = useState(1)
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>
)
const loadMore = async () => {
setPage(page + 1)
reload();
}
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>
)
const reload = () => {
pageUserBalanceLog({page}).then(res => {
console.log(res)
const newList = res?.list || [];
setList([...list, ...newList])
setHasMore(newList.length > 0)
})
}
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 ? '管理员操作' : '订单退款'}
<>
<ul style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={loadMore}
onScroll={() => {
console.log('onScroll')
}}
onScrollToUpper={() => {
console.log('onScrollToUpper')
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
<View className="p-4">
{list.map((item, index) => (
<Cell.Group key={`${item.logId}-${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-orange-500' : ''
}`}>
{item.scene === 10 ? '+' : '-'}
{formatCurrency(Number(item.money), 'CNY') || '0.00'}
</View>
</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>
);
};
<View className="flex justify-between w-full items-center text-xs text-gray-400 mt-2">
<View>
{item.createTime}
</View>
<View>{item?.balance}</View>
</View>
export default Wallet;
{item.remark && (
<View className="text-xs text-gray-500 mt-1 p-2 bg-gray-50 rounded">
: {item.remark}
</View>
)}
</Cell>
</Cell.Group>
))}
</View>
</InfiniteLoading>
</ul>
</>
)
}
export default Wallet

4
src/utils/server.ts

@ -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

Loading…
Cancel
Save