feat(admin): 从文章详情页面改为文章管理页面
- 修改页面配置,设置新的导航栏标题和样式 - 重新设计页面布局,增加搜索栏、文章列表和操作按钮 - 添加文章搜索、分页加载和删除功能 - 优化文章列表项的样式和交互 - 新增礼品卡相关API和组件 - 更新优惠券组件,增加到期提醒和筛选功能
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '文章详情'
|
||||
navigationBarTitleText: '商品文章管理',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
@@ -1,53 +1,271 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {View, RichText} from '@tarojs/components'
|
||||
import {wxParse} from "@/utils/common";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model"
|
||||
import Line from "@/components/Gap";
|
||||
import './index.scss'
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import {pageCmsArticle, removeCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
function Detail() {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
// 文章详情
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const reload = async () => {
|
||||
const item = await getCmsArticle(Number(params.id))
|
||||
const ArticleArticleManage = () => {
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
// const [refreshing, setRefreshing] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
if (item) {
|
||||
item.content = wxParse(item.content)
|
||||
setItem(item)
|
||||
Taro.setNavigationBarTitle({
|
||||
title: `${item?.categoryName}`
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
const res = await pageCmsArticle({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: searchValue
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
setTotal(res.count || 0)
|
||||
|
||||
// 判断是否还有更多数据
|
||||
setHasMore(res.list.length === 10) // 如果返回的数据等于limit,说明可能还有更多
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2) // 刷新后下一页是第2页
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文章失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取文章失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2'}>加载中</Loading>
|
||||
)
|
||||
// 搜索功能
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchValue(value)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'bg-white'}>
|
||||
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
|
||||
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
|
||||
<View className={'content p-4'}>
|
||||
<RichText nodes={item?.content}/>
|
||||
</View>
|
||||
<Line height={44}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
// setRefreshing(true)
|
||||
await reload(true)
|
||||
// setRefreshing(false)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
// 删除文章
|
||||
const handleDelete = async (id?: number) => {
|
||||
Taro.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这篇文章吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await removeCmsArticle(id)
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload(true);
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
const handleEdit = (item: CmsArticle) => {
|
||||
Taro.navigateTo({
|
||||
url: `/shop/shopArticle/add?id=${item.articleId}`
|
||||
});
|
||||
}
|
||||
|
||||
// 查看文章详情
|
||||
const handleView = (item: CmsArticle) => {
|
||||
// 这里可以跳转到文章详情页面
|
||||
Taro.navigateTo({
|
||||
url: `/cms/detail/index?id=${item.articleId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusTag = (status?: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return <Tag type="success">已发布</Tag>
|
||||
case 1:
|
||||
return <Tag type="warning">待审核</Tag>
|
||||
case 2:
|
||||
return <Tag type="danger">已驳回</Tag>
|
||||
case 3:
|
||||
return <Tag type="danger">违规内容</Tag>
|
||||
default:
|
||||
return <Tag>未知</Tag>
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false) // 不刷新,追加数据
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 搜索栏 */}
|
||||
<View className="py-2">
|
||||
<SearchBar
|
||||
placeholder="搜索关键词"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{total > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500">
|
||||
共找到 {total} 篇文章
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 文章列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View className="px-4" style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="article-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 200px)'}}>
|
||||
<Empty
|
||||
description="暂无文章数据"
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="article-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
{list.map((item, index) => (
|
||||
<CellGroup key={item.articleId || index} className="mb-4">
|
||||
<Cell>
|
||||
<View className="flex flex-col gap-3 w-full">
|
||||
{/* 文章标题和状态 */}
|
||||
<View className="flex justify-between items-start">
|
||||
<View className="flex-1 pr-2">
|
||||
<View className="text-lg font-bold text-gray-900 line-clamp-2">
|
||||
{item.title}
|
||||
</View>
|
||||
</View>
|
||||
{getStatusTag(item.status)}
|
||||
</View>
|
||||
|
||||
{/* 文章概述 */}
|
||||
{item.overview && (
|
||||
<View className="text-sm text-gray-600 line-clamp-2">
|
||||
{item.overview}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 文章信息 */}
|
||||
<View className="flex justify-between items-center text-xs text-gray-500">
|
||||
<View className="flex items-center gap-4">
|
||||
<View>阅读: {item.actualViews || 0}</View>
|
||||
{item.price && <View>价格: ¥{item.price}</View>}
|
||||
<View>创建: {dayjs(item.createTime).format('MM-DD HH:mm')}</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="flex justify-end gap-2 pt-2 border-t border-gray-100">
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Eye/>}
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Edit/>}
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="danger"
|
||||
fill="outline"
|
||||
icon={<Del/>}
|
||||
onClick={() => handleDelete(item.articleId)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
</CellGroup>
|
||||
))}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton
|
||||
text="发布文章"
|
||||
icon={<Edit />}
|
||||
onClick={() => Taro.navigateTo({url: '/shop/shopArticle/add'})}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ArticleArticleManage;
|
||||
|
||||
@@ -84,8 +84,6 @@ export interface ShopArticle {
|
||||
bmUsers?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 商户ID
|
||||
merchantId?: number;
|
||||
// 项目ID
|
||||
projectId?: number;
|
||||
// 语言
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface ShopCoupon {
|
||||
limitPerUser?: number;
|
||||
// 是否启用(0禁用 1启用)
|
||||
enabled?: string;
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
180
src/api/shop/shopGift/index.ts
Normal file
180
src/api/shop/shopGift/index.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api/index';
|
||||
import type { ShopGift, ShopGiftParam, GiftRedeemParam, GiftUseParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询礼品卡
|
||||
*/
|
||||
export async function pageShopGift(params: ShopGiftParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopGift>>>(
|
||||
'/shop/shop-gift/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询礼品卡列表
|
||||
*/
|
||||
export async function listShopGift(params?: ShopGiftParam) {
|
||||
const res = await request.get<ApiResult<ShopGift[]>>(
|
||||
'/shop/shop-gift',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加礼品卡
|
||||
*/
|
||||
export async function addShopGift(data: ShopGift) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成礼品卡
|
||||
*/
|
||||
export async function makeShopGift(data: ShopGift) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/make',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改礼品卡
|
||||
*/
|
||||
export async function updateShopGift(data: ShopGift) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-gift',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼品卡
|
||||
*/
|
||||
export async function removeShopGift(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除礼品卡
|
||||
*/
|
||||
export async function removeBatchShopGift(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询礼品卡
|
||||
*/
|
||||
export async function getShopGift(id: number) {
|
||||
const res = await request.get<ApiResult<ShopGift>>(
|
||||
'/shop/shop-gift/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 兑换礼品卡
|
||||
*/
|
||||
export async function redeemGift(params: GiftRedeemParam) {
|
||||
const res = await request.post<ApiResult<ShopGift>>(
|
||||
'/shop/shop-gift/redeem',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用礼品卡
|
||||
*/
|
||||
export async function useGift(params: GiftUseParam) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/use',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的礼品卡列表
|
||||
*/
|
||||
export async function getUserGifts(params: ShopGiftParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopGift>>>(
|
||||
'/shop/shop-gift/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证礼品卡兑换码
|
||||
*/
|
||||
export async function validateGiftCode(code: string) {
|
||||
const res = await request.get<ApiResult<ShopGift>>(
|
||||
`/shop/shop-gift/validate/${code}`
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
export async function exportShopGift(ids?: number[]) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/export',
|
||||
ids
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
115
src/api/shop/shopGift/model/index.ts
Normal file
115
src/api/shop/shopGift/model/index.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 礼品卡
|
||||
*/
|
||||
export interface ShopGift {
|
||||
// 礼品卡ID
|
||||
id?: number;
|
||||
// 礼品卡名称
|
||||
name?: string;
|
||||
// 礼品卡描述
|
||||
description?: string;
|
||||
// 礼品卡兑换码
|
||||
code?: string;
|
||||
// 关联商品ID
|
||||
goodsId?: number;
|
||||
// 商品名称
|
||||
goodsName?: string;
|
||||
// 商品图片
|
||||
goodsImage?: string;
|
||||
// 礼品卡面值
|
||||
faceValue?: string;
|
||||
// 礼品卡类型 (10实物礼品卡 20虚拟礼品卡 30服务礼品卡)
|
||||
type?: number;
|
||||
// 领取时间
|
||||
takeTime?: string;
|
||||
// 使用时间
|
||||
useTime?: string;
|
||||
// 过期时间
|
||||
expireTime?: string;
|
||||
// 有效期天数
|
||||
validDays?: number;
|
||||
// 操作人
|
||||
operatorUserId?: number;
|
||||
// 是否展示
|
||||
isShow?: string;
|
||||
// 状态 (0未使用 1已使用 2已过期 3已失效)
|
||||
status?: number;
|
||||
// 使用状态 (0可用 1已使用 2已过期)
|
||||
useStatus?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 使用说明
|
||||
instructions?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 拥有者用户ID
|
||||
userId?: number;
|
||||
// 发放者用户ID
|
||||
issuerUserId?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 数量
|
||||
num?: number;
|
||||
// 已发放数量
|
||||
issuedCount?: number;
|
||||
// 总发放数量
|
||||
totalCount?: number;
|
||||
// 使用门店/地址
|
||||
useLocation?: string;
|
||||
// 客服联系方式
|
||||
contactInfo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼品卡搜索条件
|
||||
*/
|
||||
export interface ShopGiftParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
// 礼品卡类型筛选
|
||||
type?: number;
|
||||
// 状态筛选
|
||||
status?: number;
|
||||
// 使用状态筛选
|
||||
useStatus?: number;
|
||||
// 用户ID筛选
|
||||
userId?: number;
|
||||
// 商品ID筛选
|
||||
goodsId?: number;
|
||||
// 是否过期筛选
|
||||
isExpired?: boolean;
|
||||
// 排序字段
|
||||
sortBy?: 'createTime' | 'expireTime' | 'faceValue' | 'useTime';
|
||||
// 排序方向
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼品卡兑换参数
|
||||
*/
|
||||
export interface GiftRedeemParam {
|
||||
// 兑换码
|
||||
code: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼品卡使用参数
|
||||
*/
|
||||
export interface GiftUseParam {
|
||||
// 礼品卡ID
|
||||
giftId: number;
|
||||
// 使用地址/门店
|
||||
useLocation?: string;
|
||||
// 使用备注
|
||||
useNote?: string;
|
||||
}
|
||||
@@ -38,8 +38,9 @@ export default defineAppConfig({
|
||||
"help/index",
|
||||
"about/index",
|
||||
"wallet/wallet",
|
||||
"coupon/coupon",
|
||||
"points/points"
|
||||
// "coupon/index",
|
||||
// "points/points",
|
||||
"gift/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -54,15 +55,17 @@ export default defineAppConfig({
|
||||
},
|
||||
{
|
||||
"root": "shop",
|
||||
"pages": [
|
||||
'category/index',
|
||||
"pages": ['category/index',
|
||||
'orderDetail/index',
|
||||
'goodsDetail/index',
|
||||
'orderConfirm/index',
|
||||
'orderConfirmCart/index',
|
||||
'search/index',
|
||||
'shopArticle/index',
|
||||
'shopArticle/add'
|
||||
'search/index']
|
||||
},
|
||||
{
|
||||
"root": "admin",
|
||||
"pages": [
|
||||
"article/index",
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
height: 120px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
@@ -15,7 +21,7 @@
|
||||
|
||||
.coupon-left {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
width: 110px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -24,23 +30,23 @@
|
||||
position: relative;
|
||||
|
||||
&.theme-red {
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||
}
|
||||
|
||||
&.theme-orange {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||
}
|
||||
|
||||
&.theme-blue {
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
|
||||
}
|
||||
|
||||
&.theme-purple {
|
||||
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
||||
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
|
||||
}
|
||||
|
||||
&.theme-green {
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||
}
|
||||
|
||||
.amount-wrapper {
|
||||
@@ -49,22 +55,23 @@
|
||||
margin-bottom: 8px;
|
||||
|
||||
.currency {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 30px;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.condition {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
opacity: 0.9;
|
||||
margin-top: 2px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,21 +119,23 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
|
||||
.coupon-info {
|
||||
flex: 1;
|
||||
|
||||
.coupon-title {
|
||||
font-size: 28px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.coupon-validity {
|
||||
font-size: 24px;
|
||||
font-size: 26px;
|
||||
color: #6b7280;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,38 +145,45 @@
|
||||
align-items: center;
|
||||
|
||||
.coupon-btn {
|
||||
min-width: 48px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 24px;
|
||||
min-width: 120px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
font-size: 26px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.theme-red {
|
||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||
}
|
||||
|
||||
&.theme-orange {
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
||||
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||
}
|
||||
|
||||
&.theme-blue {
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||||
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
|
||||
}
|
||||
|
||||
&.theme-purple {
|
||||
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
||||
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
|
||||
}
|
||||
|
||||
&.theme-green {
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
color: #6b7280;
|
||||
padding: 4px 8px;
|
||||
font-size: 26px;
|
||||
color: #9ca3af;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +79,41 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
|
||||
// 获取使用条件文本
|
||||
const getConditionText = () => {
|
||||
if (type === 3) return '无门槛'
|
||||
if (type === 3) return '免费使用' // 免费券
|
||||
if (minAmount && minAmount > 0) {
|
||||
return `满${minAmount}可用`
|
||||
return `满${minAmount}元可用`
|
||||
}
|
||||
return '无门槛'
|
||||
}
|
||||
|
||||
// 格式化有效期显示
|
||||
const formatValidityPeriod = () => {
|
||||
if (!startTime || !endTime) return ''
|
||||
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
const now = new Date()
|
||||
|
||||
// 如果还未开始
|
||||
if (now < start) {
|
||||
return `${start.getMonth() + 1}.${start.getDate()} 开始生效`
|
||||
}
|
||||
|
||||
// 计算剩余天数
|
||||
const diffTime = end.getTime() - now.getTime()
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays <= 0) {
|
||||
return '已过期'
|
||||
} else if (diffDays <= 3) {
|
||||
return `${diffDays}天后过期`
|
||||
} else {
|
||||
return `${end.getMonth() + 1}.${end.getDate()} 过期`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return ''
|
||||
@@ -108,8 +136,8 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
{/* 左侧金额区域 */}
|
||||
<View className={`coupon-left ${themeClass}`}>
|
||||
<View className="amount-wrapper">
|
||||
<Text className="currency">¥</Text>
|
||||
<Text className="amount">{amount}</Text>
|
||||
{type !== 3 && <Text className="currency">¥</Text>}
|
||||
<Text className="amount">{formatAmount()}</Text>
|
||||
</View>
|
||||
<View className="condition">
|
||||
{getConditionText()}
|
||||
@@ -130,7 +158,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')}
|
||||
</View>
|
||||
<View className="coupon-validity">
|
||||
有效期:{getValidityText()}
|
||||
{formatValidityPeriod()}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -151,7 +179,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
||||
size="small"
|
||||
onClick={onUse}
|
||||
>
|
||||
使用
|
||||
立即使用
|
||||
</Button>
|
||||
)}
|
||||
{status !== 0 && (
|
||||
|
||||
174
src/components/CouponExpireNotice.tsx
Normal file
174
src/components/CouponExpireNotice.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Clock, Close, Agenda } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface ExpiringSoon {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
amount: string
|
||||
minAmount?: string
|
||||
endTime: string
|
||||
daysLeft: number
|
||||
}
|
||||
|
||||
export interface CouponExpireNoticeProps {
|
||||
/** 是否显示提醒 */
|
||||
visible: boolean
|
||||
/** 即将过期的优惠券列表 */
|
||||
expiringSoonCoupons: ExpiringSoon[]
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
/** 使用优惠券回调 */
|
||||
onUseCoupon: (coupon: ExpiringSoon) => void
|
||||
}
|
||||
|
||||
const CouponExpireNotice: React.FC<CouponExpireNoticeProps> = ({
|
||||
visible,
|
||||
expiringSoonCoupons,
|
||||
onClose,
|
||||
onUseCoupon
|
||||
}) => {
|
||||
// 获取优惠券金额显示
|
||||
const getCouponAmountDisplay = (coupon: ExpiringSoon) => {
|
||||
switch (coupon.type) {
|
||||
case 10: // 满减券
|
||||
return `¥${coupon.amount}`
|
||||
case 20: // 折扣券
|
||||
return `${coupon.amount}折`
|
||||
case 30: // 免费券
|
||||
return '免费'
|
||||
default:
|
||||
return `¥${coupon.amount}`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取使用条件文本
|
||||
const getConditionText = (coupon: ExpiringSoon) => {
|
||||
if (coupon.type === 30) return '无门槛使用'
|
||||
if (coupon.minAmount && parseFloat(coupon.minAmount) > 0) {
|
||||
return `满${coupon.minAmount}元可用`
|
||||
}
|
||||
return '无门槛使用'
|
||||
}
|
||||
|
||||
// 获取到期时间显示
|
||||
const getExpireTimeDisplay = (coupon: ExpiringSoon) => {
|
||||
if (coupon.daysLeft === 0) {
|
||||
return '今天到期'
|
||||
} else if (coupon.daysLeft === 1) {
|
||||
return '明天到期'
|
||||
} else {
|
||||
return `${coupon.daysLeft}天后到期`
|
||||
}
|
||||
}
|
||||
|
||||
// 去购物
|
||||
const handleGoShopping = () => {
|
||||
onClose()
|
||||
Taro.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
style={{ width: '90%', maxWidth: '400px' }}
|
||||
round
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<View className="flex items-center">
|
||||
<Clock size="24" className="text-orange-500 mr-2" />
|
||||
<Text className="text-lg font-semibold text-gray-900">
|
||||
优惠券即将过期
|
||||
</Text>
|
||||
</View>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-500" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 提醒文案 */}
|
||||
<View className="text-center mb-6">
|
||||
<Text className="text-gray-600">
|
||||
您有 {expiringSoonCoupons.length} 张优惠券即将过期,请及时使用
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 优惠券列表 */}
|
||||
<View className="max-h-80 overflow-y-auto mb-6">
|
||||
{expiringSoonCoupons.map((coupon, _) => (
|
||||
<View
|
||||
key={coupon.id}
|
||||
className="flex items-center justify-between p-3 mb-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center mb-1">
|
||||
<Text className="font-semibold text-gray-900 mr-2">
|
||||
{coupon.name}
|
||||
</Text>
|
||||
<View className="px-2 py-1 bg-red-100 rounded text-red-600 text-xs">
|
||||
{getCouponAmountDisplay(coupon)}
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-sm text-gray-600 mb-1">
|
||||
{getConditionText(coupon)}
|
||||
</Text>
|
||||
<Text className={`text-xs ${
|
||||
coupon.daysLeft === 0 ? 'text-red-500' :
|
||||
coupon.daysLeft === 1 ? 'text-orange-500' : 'text-gray-500'
|
||||
}`}>
|
||||
{getExpireTimeDisplay(coupon)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => onUseCoupon(coupon)}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="flex gap-3">
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={onClose}
|
||||
>
|
||||
稍后提醒
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
icon={<Agenda />}
|
||||
onClick={handleGoShopping}
|
||||
>
|
||||
去购物
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<View className="text-center mt-4">
|
||||
<Text className="text-xs text-gray-400">
|
||||
过期的优惠券将无法使用,请及时关注有效期
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponExpireNotice
|
||||
210
src/components/CouponFilter.tsx
Normal file
210
src/components/CouponFilter.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup, Radio, RadioGroup, Divider } from '@nutui/nutui-react-taro'
|
||||
import { Filter, Close } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface CouponFilterProps {
|
||||
/** 是否显示筛选器 */
|
||||
visible: boolean
|
||||
/** 当前筛选条件 */
|
||||
filters: {
|
||||
type?: number[]
|
||||
minAmount?: number
|
||||
sortBy?: 'createTime' | 'amount' | 'expireTime'
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
/** 筛选条件变更回调 */
|
||||
onFiltersChange: (filters: any) => void
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CouponFilter: React.FC<CouponFilterProps> = ({
|
||||
visible,
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onClose
|
||||
}) => {
|
||||
const [tempFilters, setTempFilters] = useState(filters)
|
||||
|
||||
// 优惠券类型选项
|
||||
const typeOptions = [
|
||||
{ label: '全部类型', value: '' },
|
||||
{ label: '满减券', value: '10' },
|
||||
{ label: '折扣券', value: '20' },
|
||||
{ label: '免费券', value: '30' }
|
||||
]
|
||||
|
||||
// 最低金额选项
|
||||
const minAmountOptions = [
|
||||
{ label: '不限', value: '' },
|
||||
{ label: '10元以上', value: '10' },
|
||||
{ label: '50元以上', value: '50' },
|
||||
{ label: '100元以上', value: '100' },
|
||||
{ label: '200元以上', value: '200' }
|
||||
]
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = [
|
||||
{ label: '创建时间', value: 'createTime' },
|
||||
{ label: '优惠金额', value: 'amount' },
|
||||
{ label: '到期时间', value: 'expireTime' }
|
||||
]
|
||||
|
||||
// 排序方向选项
|
||||
const sortOrderOptions = [
|
||||
{ label: '升序', value: 'asc' },
|
||||
{ label: '降序', value: 'desc' }
|
||||
]
|
||||
|
||||
// 重置筛选条件
|
||||
const handleReset = () => {
|
||||
const resetFilters = {
|
||||
type: [],
|
||||
minAmount: undefined,
|
||||
sortBy: 'createTime' as const,
|
||||
sortOrder: 'desc' as const
|
||||
}
|
||||
setTempFilters(resetFilters)
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
const handleApply = () => {
|
||||
onFiltersChange(tempFilters)
|
||||
onClose()
|
||||
}
|
||||
|
||||
// 更新临时筛选条件
|
||||
const updateTempFilters = (key: string, value: any) => {
|
||||
setTempFilters(prev => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="right"
|
||||
style={{ width: '80%', height: '100%' }}
|
||||
>
|
||||
<View className="h-full flex flex-col">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<View className="flex items-center">
|
||||
<Filter size="20" className="text-gray-600 mr-2" />
|
||||
<Text className="text-lg font-semibold">筛选条件</Text>
|
||||
</View>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-600" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 筛选内容 */}
|
||||
<View className="flex-1 overflow-y-auto p-4">
|
||||
{/* 优惠券类型 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
优惠券类型
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.type?.[0]?.toString() || ''}
|
||||
onChange={(value) => {
|
||||
updateTempFilters('type', value ? [parseInt(value)] : [])
|
||||
}}
|
||||
>
|
||||
{typeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 最低消费金额 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
最低消费金额
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.minAmount?.toString() || ''}
|
||||
onChange={(value) => {
|
||||
updateTempFilters('minAmount', value ? parseInt(value) : undefined)
|
||||
}}
|
||||
>
|
||||
{minAmountOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 排序方式 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
排序方式
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.sortBy || 'createTime'}
|
||||
onChange={(value) => updateTempFilters('sortBy', value)}
|
||||
>
|
||||
{sortOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 排序方向 */}
|
||||
<View className="mb-6">
|
||||
<Text className="text-base font-semibold mb-3 text-gray-900">
|
||||
排序方向
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={tempFilters.sortOrder || 'desc'}
|
||||
onChange={(value) => updateTempFilters('sortOrder', value)}
|
||||
>
|
||||
{sortOrderOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="mb-2">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="p-4 border-t border-gray-100">
|
||||
<View className="flex gap-3">
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={handleApply}
|
||||
>
|
||||
应用
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponFilter
|
||||
159
src/components/CouponGuide.tsx
Normal file
159
src/components/CouponGuide.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Ask, Ticket, Clock, Gift } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface CouponGuideProps {
|
||||
/** 是否显示指南 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CouponGuide: React.FC<CouponGuideProps> = ({
|
||||
visible,
|
||||
onClose
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const guideSteps = [
|
||||
{
|
||||
title: '如何获取优惠券?',
|
||||
icon: <Gift size="24" className="text-red-500" />,
|
||||
content: [
|
||||
'1. 点击"领取"按钮浏览可领取的优惠券',
|
||||
'2. 关注商家活动和促销信息',
|
||||
'3. 完成指定任务获得优惠券奖励',
|
||||
'4. 邀请好友注册获得推荐奖励'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '如何使用优惠券?',
|
||||
icon: <Ticket size="24" className="text-green-500" />,
|
||||
content: [
|
||||
'1. 选择心仪商品加入购物车',
|
||||
'2. 在结算页面选择可用优惠券',
|
||||
'3. 确认优惠金额后完成支付',
|
||||
'4. 优惠券使用后不可退回'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '优惠券使用规则',
|
||||
icon: <Clock size="24" className="text-blue-500" />,
|
||||
content: [
|
||||
'1. 每张优惠券只能使用一次',
|
||||
'2. 优惠券有使用期限,过期作废',
|
||||
'3. 满减券需达到最低消费金额',
|
||||
'4. 部分优惠券仅限指定商品使用'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '常见问题解答',
|
||||
icon: <Ask size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'Q: 优惠券可以叠加使用吗?',
|
||||
'A: 一般情况下不支持叠加,具体以活动规则为准',
|
||||
'Q: 优惠券过期了怎么办?',
|
||||
'A: 过期优惠券无法使用,请及时关注有效期',
|
||||
'Q: 退款时优惠券会退回吗?',
|
||||
'A: 已使用的优惠券不会退回,请谨慎使用'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < guideSteps.length - 1) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const currentGuide = guideSteps[currentStep]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable={false}
|
||||
style={{ width: '90%', maxWidth: '400px' }}
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="text-center mb-6">
|
||||
<View className="flex justify-center mb-3">
|
||||
{currentGuide.icon}
|
||||
</View>
|
||||
<Text className="text-xl font-bold text-gray-900">
|
||||
{currentGuide.title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className="mb-6">
|
||||
{currentGuide.content.map((item, index) => (
|
||||
<View key={index} className="mb-3">
|
||||
<Text className="text-gray-700 leading-relaxed">
|
||||
{item}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 进度指示器 */}
|
||||
<View className="flex justify-center mb-6">
|
||||
{guideSteps.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full mx-1 ${
|
||||
index === currentStep ? 'bg-red-500' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="flex justify-between">
|
||||
<View className="flex gap-2">
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handleSkip}
|
||||
>
|
||||
跳过
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
>
|
||||
{currentStep === guideSteps.length - 1 ? '完成' : '下一步'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponGuide
|
||||
182
src/components/CouponShare.tsx
Normal file
182
src/components/CouponShare.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Share, Wechat, QQ, Weibo, Link, Close } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface CouponShareProps {
|
||||
/** 是否显示分享弹窗 */
|
||||
visible: boolean
|
||||
/** 优惠券信息 */
|
||||
coupon: {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
amount: string
|
||||
minAmount?: string
|
||||
description?: string
|
||||
}
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const CouponShare: React.FC<CouponShareProps> = ({
|
||||
visible,
|
||||
coupon,
|
||||
onClose
|
||||
}) => {
|
||||
// 生成分享文案
|
||||
const generateShareText = () => {
|
||||
const typeText = coupon.type === 10 ? '满减券' : coupon.type === 20 ? '折扣券' : '免费券'
|
||||
const amountText = coupon.type === 10 ? `¥${coupon.amount}` :
|
||||
coupon.type === 20 ? `${coupon.amount}折` : '免费'
|
||||
const conditionText = coupon.minAmount ? `满${coupon.minAmount}元可用` : '无门槛'
|
||||
|
||||
return `🎁 ${coupon.name}\n💰 ${amountText} ${typeText}\n📋 ${conditionText}\n快来领取吧!`
|
||||
}
|
||||
|
||||
// 生成分享链接
|
||||
const generateShareUrl = () => {
|
||||
// 这里应该是实际的分享链接,包含优惠券ID等参数
|
||||
return `https://your-domain.com/coupon/share?id=${coupon.id}`
|
||||
}
|
||||
|
||||
// 微信分享
|
||||
const handleWechatShare = () => {
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
const handleCopyLink = () => {
|
||||
const shareUrl = generateShareUrl()
|
||||
const shareText = generateShareText()
|
||||
const fullText = `${shareText}\n\n${shareUrl}`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: fullText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存图片分享
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
// 这里可以生成优惠券图片并保存到相册
|
||||
// 实际实现需要canvas绘制优惠券图片
|
||||
Taro.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const shareOptions = [
|
||||
{
|
||||
icon: <Wechat size="32" className="text-green-500" />,
|
||||
label: '微信好友',
|
||||
onClick: handleWechatShare
|
||||
},
|
||||
{
|
||||
icon: <Link size="32" className="text-blue-500" />,
|
||||
label: '复制链接',
|
||||
onClick: handleCopyLink
|
||||
},
|
||||
{
|
||||
icon: <Share size="32" className="text-purple-500" />,
|
||||
label: '保存图片',
|
||||
onClick: handleSaveImage
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
style={{ height: 'auto' }}
|
||||
round
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between mb-6">
|
||||
<Text className="text-lg font-semibold">分享优惠券</Text>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-500" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 优惠券预览 */}
|
||||
<View className="bg-gradient-to-r from-red-400 to-red-500 rounded-xl p-4 mb-6 text-white">
|
||||
<Text className="text-xl font-bold mb-2">{coupon.name}</Text>
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold">
|
||||
{coupon.type === 10 ? `¥${coupon.amount}` :
|
||||
coupon.type === 20 ? `${coupon.amount}折` : '免费'}
|
||||
</Text>
|
||||
<Text className="text-sm opacity-90">
|
||||
{coupon.minAmount ? `满${coupon.minAmount}元可用` : '无门槛使用'}
|
||||
</Text>
|
||||
</View>
|
||||
<Share size="24" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 分享选项 */}
|
||||
<View className="grid grid-cols-3 gap-4 mb-4">
|
||||
{shareOptions.map((option, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className="flex flex-col items-center py-4 bg-gray-50 rounded-lg"
|
||||
onClick={option.onClick}
|
||||
>
|
||||
<View className="mb-2">{option.icon}</View>
|
||||
<Text className="text-sm text-gray-700">{option.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 分享文案预览 */}
|
||||
<View className="bg-gray-50 rounded-lg p-3">
|
||||
<Text className="text-xs text-gray-500 mb-2">分享文案预览:</Text>
|
||||
<Text className="text-sm text-gray-700 leading-relaxed">
|
||||
{generateShareText()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponShare
|
||||
71
src/components/CouponStats.tsx
Normal file
71
src/components/CouponStats.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Gift, Voucher, Clock } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface CouponStatsProps {
|
||||
/** 可用优惠券数量 */
|
||||
availableCount: number
|
||||
/** 已使用优惠券数量 */
|
||||
usedCount: number
|
||||
/** 已过期优惠券数量 */
|
||||
expiredCount: number
|
||||
/** 点击统计项的回调 */
|
||||
onStatsClick?: (type: 'available' | 'used' | 'expired') => void
|
||||
}
|
||||
|
||||
const CouponStats: React.FC<CouponStatsProps> = ({
|
||||
availableCount,
|
||||
usedCount,
|
||||
expiredCount,
|
||||
onStatsClick
|
||||
}) => {
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired') => {
|
||||
onStatsClick?.(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-white mx-4 my-3 rounded-xl p-4">
|
||||
<Text className="text-lg font-semibold mb-4 text-gray-800">优惠券统计</Text>
|
||||
|
||||
<View className="flex justify-between">
|
||||
{/* 可用优惠券 */}
|
||||
<View
|
||||
className="flex-1 text-center py-3 mx-1 bg-red-50 rounded-lg"
|
||||
onClick={() => handleStatsClick('available')}
|
||||
>
|
||||
<View className="flex justify-center mb-2">
|
||||
<Gift size="24" className="text-red-500" />
|
||||
</View>
|
||||
<Text className="text-2xl font-bold text-red-500 block">{availableCount}</Text>
|
||||
<Text className="text-sm text-gray-600 mt-1">可用</Text>
|
||||
</View>
|
||||
|
||||
{/* 已使用优惠券 */}
|
||||
<View
|
||||
className="flex-1 text-center py-3 mx-1 bg-green-50 rounded-lg"
|
||||
onClick={() => handleStatsClick('used')}
|
||||
>
|
||||
<View className="flex justify-center mb-2">
|
||||
<Voucher size="24" className="text-green-500" />
|
||||
</View>
|
||||
<Text className="text-2xl font-bold text-green-500 block">{usedCount}</Text>
|
||||
<Text className="text-sm text-gray-600 mt-1">已使用</Text>
|
||||
</View>
|
||||
|
||||
{/* 已过期优惠券 */}
|
||||
<View
|
||||
className="flex-1 text-center py-3 mx-1 bg-gray-50 rounded-lg"
|
||||
onClick={() => handleStatsClick('expired')}
|
||||
>
|
||||
<View className="flex justify-center mb-2">
|
||||
<Clock size="24" className="text-gray-500" />
|
||||
</View>
|
||||
<Text className="text-2xl font-bold text-gray-500 block">{expiredCount}</Text>
|
||||
<Text className="text-sm text-gray-600 mt-1">已过期</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponStats
|
||||
162
src/components/CouponUsageRecord.tsx
Normal file
162
src/components/CouponUsageRecord.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Tag } from '@nutui/nutui-react-taro'
|
||||
import { Voucher, Clock, Agenda } from '@nutui/icons-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export interface CouponUsageRecordProps {
|
||||
/** 优惠券ID */
|
||||
couponId: number
|
||||
/** 优惠券名称 */
|
||||
couponName: string
|
||||
/** 优惠券类型 */
|
||||
couponType: number
|
||||
/** 优惠券金额 */
|
||||
couponAmount: string
|
||||
/** 使用时间 */
|
||||
usedTime: string
|
||||
/** 订单号 */
|
||||
orderNo?: string
|
||||
/** 订单金额 */
|
||||
orderAmount?: string
|
||||
/** 节省金额 */
|
||||
savedAmount?: string
|
||||
/** 使用状态:1-已使用 2-已过期 */
|
||||
status: 1 | 2
|
||||
/** 点击事件 */
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const CouponUsageRecord: React.FC<CouponUsageRecordProps> = ({
|
||||
couponName,
|
||||
couponType,
|
||||
couponAmount,
|
||||
usedTime,
|
||||
orderNo,
|
||||
orderAmount,
|
||||
savedAmount,
|
||||
status,
|
||||
onClick
|
||||
}) => {
|
||||
// 获取优惠券类型文本
|
||||
const getCouponTypeText = () => {
|
||||
switch (couponType) {
|
||||
case 10: return '满减券'
|
||||
case 20: return '折扣券'
|
||||
case 30: return '免费券'
|
||||
default: return '优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠券金额显示
|
||||
const getCouponAmountDisplay = () => {
|
||||
switch (couponType) {
|
||||
case 10: // 满减券
|
||||
return `¥${couponAmount}`
|
||||
case 20: // 折扣券
|
||||
return `${couponAmount}折`
|
||||
case 30: // 免费券
|
||||
return '免费'
|
||||
default:
|
||||
return `¥${couponAmount}`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = () => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return {
|
||||
text: '已使用',
|
||||
color: 'success' as const,
|
||||
icon: <Voucher size="16" className="text-green-500" />
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
text: '已过期',
|
||||
color: 'danger' as const,
|
||||
icon: <Clock size="16" className="text-red-500" />
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: '未知',
|
||||
color: 'default' as const,
|
||||
icon: <Clock size="16" className="text-gray-500" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo()
|
||||
|
||||
return (
|
||||
<View
|
||||
className="bg-white mx-4 mb-3 rounded-xl p-4 border border-gray-100"
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 头部信息 */}
|
||||
<View className="flex items-center justify-between mb-3">
|
||||
<View className="flex items-center">
|
||||
<View className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<Text className="text-red-500 font-bold text-lg">
|
||||
{getCouponAmountDisplay()}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-900 text-base">{couponName}</Text>
|
||||
<Text className="text-gray-500 text-sm">{getCouponTypeText()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center">
|
||||
{statusInfo.icon}
|
||||
<Tag type={statusInfo.color} className="ml-2">
|
||||
{statusInfo.text}
|
||||
</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 使用详情 */}
|
||||
<View className="bg-gray-50 rounded-lg p-3">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-gray-600 text-sm">使用时间</Text>
|
||||
<Text className="text-gray-900 text-sm">
|
||||
{dayjs(usedTime).format('YYYY-MM-DD HH:mm')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{orderNo && (
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-gray-600 text-sm">订单号</Text>
|
||||
<Text className="text-gray-900 text-sm">{orderNo}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{orderAmount && (
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="text-gray-600 text-sm">订单金额</Text>
|
||||
<Text className="text-gray-900 text-sm">¥{orderAmount}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{savedAmount && (
|
||||
<View className="flex items-center justify-between">
|
||||
<Text className="text-gray-600 text-sm">节省金额</Text>
|
||||
<Text className="text-red-500 text-sm font-semibold">¥{savedAmount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部操作 */}
|
||||
{orderNo && (
|
||||
<View className="flex justify-end mt-3">
|
||||
<View className="flex items-center text-blue-500 text-sm">
|
||||
<Agenda size="14" className="mr-1" />
|
||||
<Text>查看订单</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CouponUsageRecord
|
||||
307
src/components/GiftCard.scss
Normal file
307
src/components/GiftCard.scss
Normal file
@@ -0,0 +1,307 @@
|
||||
.gift-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// 主题色彩
|
||||
&.gift-card-gold {
|
||||
.gift-card-header {
|
||||
background: #ffd700;
|
||||
}
|
||||
.use-btn {
|
||||
background: #ffd700;
|
||||
border: none;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-silver {
|
||||
.gift-card-header {
|
||||
background: #c0c0c0;
|
||||
}
|
||||
.use-btn {
|
||||
background: #c0c0c0;
|
||||
border: none;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-bronze {
|
||||
.gift-card-header {
|
||||
background: #cd7f32;
|
||||
}
|
||||
.use-btn {
|
||||
background: #cd7f32;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-blue {
|
||||
.gift-card-header {
|
||||
background: #4a90e2;
|
||||
}
|
||||
.use-btn {
|
||||
background: #4a90e2;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-green {
|
||||
.gift-card-header {
|
||||
background: #5cb85c;
|
||||
}
|
||||
.use-btn {
|
||||
background: #5cb85c;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.gift-card-purple {
|
||||
.gift-card-header {
|
||||
background: #9b59b6;
|
||||
}
|
||||
.use-btn {
|
||||
background: #9b59b6;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
|
||||
.gift-card-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gift-card-title {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
|
||||
.title-text {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-status {
|
||||
.nut-tag {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #333;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-body {
|
||||
padding: 20px;
|
||||
|
||||
.gift-card-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.gift-image {
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gift-info {
|
||||
flex: 1;
|
||||
|
||||
.gift-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.value-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value-amount {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.gift-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.gift-code {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 1px dashed #ddd;
|
||||
|
||||
.code-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.code-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-family: 'Courier New', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-time-info {
|
||||
.time-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px 20px;
|
||||
|
||||
.footer-info {
|
||||
.contact-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.contact-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.use-btn {
|
||||
min-width: 80px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
.overlay-badge {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 礼品卡基础样式
|
||||
.gift-card {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
// 使用按钮效果
|
||||
.use-btn {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.gift-card {
|
||||
.gift-card-body {
|
||||
padding: 16px;
|
||||
|
||||
.gift-card-content {
|
||||
.gift-info {
|
||||
.gift-value {
|
||||
.value-amount {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift-card-footer {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
282
src/components/GiftCard.tsx
Normal file
282
src/components/GiftCard.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import React from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Button, Tag } from '@nutui/nutui-react-taro'
|
||||
import { Gift, Clock, Location, Phone } from '@nutui/icons-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
import './GiftCard.scss'
|
||||
|
||||
export interface GiftCardProps {
|
||||
/** 礼品卡ID */
|
||||
id: number
|
||||
/** 礼品卡名称 */
|
||||
name: string
|
||||
/** 礼品卡描述 */
|
||||
description?: string
|
||||
/** 礼品卡兑换码 */
|
||||
code?: string
|
||||
/** 商品图片 */
|
||||
goodsImage?: string
|
||||
/** 礼品卡面值 */
|
||||
faceValue?: string
|
||||
/** 礼品卡类型:10-实物礼品卡 20-虚拟礼品卡 30-服务礼品卡 */
|
||||
type?: number
|
||||
/** 使用状态:0-可用 1-已使用 2-已过期 */
|
||||
useStatus?: number
|
||||
/** 过期时间 */
|
||||
expireTime?: string
|
||||
/** 使用时间 */
|
||||
useTime?: string
|
||||
/** 使用地址 */
|
||||
useLocation?: string
|
||||
/** 客服联系方式 */
|
||||
contactInfo?: string
|
||||
/** 是否显示兑换码 */
|
||||
showCode?: boolean
|
||||
/** 是否显示使用按钮 */
|
||||
showUseBtn?: boolean
|
||||
/** 是否显示详情按钮 */
|
||||
showDetailBtn?: boolean
|
||||
/** 卡片主题色 */
|
||||
theme?: 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple'
|
||||
/** 使用按钮点击事件 */
|
||||
onUse?: () => void
|
||||
/** 详情按钮点击事件 */
|
||||
onDetail?: () => void
|
||||
/** 卡片点击事件 */
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const GiftCard: React.FC<GiftCardProps> = ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
code,
|
||||
goodsImage,
|
||||
faceValue,
|
||||
type = 10,
|
||||
useStatus = 0,
|
||||
expireTime,
|
||||
useTime,
|
||||
useLocation,
|
||||
contactInfo,
|
||||
showCode = false,
|
||||
showUseBtn = false,
|
||||
showDetailBtn = true,
|
||||
theme = 'gold',
|
||||
onUse,
|
||||
onDetail,
|
||||
onClick
|
||||
}) => {
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取使用状态信息
|
||||
const getStatusInfo = () => {
|
||||
switch (useStatus) {
|
||||
case 0:
|
||||
return {
|
||||
text: '可使用',
|
||||
color: 'success' as const,
|
||||
bgColor: 'bg-green-100',
|
||||
textColor: 'text-green-600'
|
||||
}
|
||||
case 1:
|
||||
return {
|
||||
text: '已使用',
|
||||
color: 'warning' as const,
|
||||
bgColor: 'bg-gray-100',
|
||||
textColor: 'text-gray-600'
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
text: '已过期',
|
||||
color: 'danger' as const,
|
||||
bgColor: 'bg-red-100',
|
||||
textColor: 'text-red-600'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: '未知',
|
||||
color: 'default' as const,
|
||||
bgColor: 'bg-gray-100',
|
||||
textColor: 'text-gray-600'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取主题样式类名
|
||||
const getThemeClass = () => {
|
||||
return `gift-card-${theme}`
|
||||
}
|
||||
|
||||
// 格式化过期时间显示
|
||||
const formatExpireTime = () => {
|
||||
if (!expireTime) return ''
|
||||
|
||||
const expire = dayjs(expireTime)
|
||||
const now = dayjs()
|
||||
const diffDays = expire.diff(now, 'day')
|
||||
|
||||
if (diffDays < 0) {
|
||||
return '已过期'
|
||||
} else if (diffDays === 0) {
|
||||
return '今天过期'
|
||||
} else if (diffDays <= 7) {
|
||||
return `${diffDays}天后过期`
|
||||
} else {
|
||||
return expire.format('YYYY-MM-DD 过期')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化兑换码显示
|
||||
const formatCode = () => {
|
||||
if (!code) return ''
|
||||
if (!showCode) return code.replace(/(.{4})/g, '$1 ').trim()
|
||||
return code.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo()
|
||||
|
||||
return (
|
||||
<View
|
||||
className={`gift-card ${getThemeClass()} ${useStatus !== 0 ? 'disabled' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 卡片头部 */}
|
||||
<View className="gift-card-header">
|
||||
<View className="gift-card-logo">
|
||||
<Gift size="24" className="text-white" />
|
||||
</View>
|
||||
<View className="gift-card-title">
|
||||
<Text className="title-text">{name}</Text>
|
||||
<Text className="type-text">{getTypeText()}</Text>
|
||||
</View>
|
||||
<View className="gift-card-status">
|
||||
<Tag type={statusInfo.color}>{statusInfo.text}</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 卡片主体 */}
|
||||
<View className="gift-card-body">
|
||||
<View className="gift-card-content">
|
||||
{/* 商品图片 */}
|
||||
{goodsImage && (
|
||||
<View className="gift-image">
|
||||
<Image
|
||||
src={goodsImage}
|
||||
className="w-16 h-16 rounded-lg object-cover"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="gift-info">
|
||||
{/* 面值 */}
|
||||
{faceValue && (
|
||||
<View className="gift-value">
|
||||
<Text className="value-label">面值</Text>
|
||||
<Text className="value-amount">¥{faceValue}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
{description && (
|
||||
<Text className="gift-description">{description}</Text>
|
||||
)}
|
||||
|
||||
{/* 兑换码 */}
|
||||
{code && (
|
||||
<View className="gift-code">
|
||||
<Text className="code-label">兑换码</Text>
|
||||
<Text className="code-value">{formatCode()}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 时间信息 */}
|
||||
<View className="gift-time-info">
|
||||
{useStatus === 1 && useTime && (
|
||||
<View className="time-item">
|
||||
<Clock size="14" className="text-gray-400" />
|
||||
<Text className="time-text">使用时间:{dayjs(useTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{useStatus === 0 && expireTime && (
|
||||
<View className="time-item">
|
||||
<Clock size="14" className="text-orange-500" />
|
||||
<Text className="time-text">{formatExpireTime()}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{useLocation && (
|
||||
<View className="time-item">
|
||||
<Location size="14" className="text-gray-400" />
|
||||
<Text className="time-text">使用地址:{useLocation}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 卡片底部操作 */}
|
||||
<View className="gift-card-footer">
|
||||
<View className="footer-info">
|
||||
{contactInfo && (
|
||||
<View className="contact-info">
|
||||
<Phone size="12" className="text-gray-400" />
|
||||
<Text className="contact-text">{contactInfo}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="footer-actions">
|
||||
{showDetailBtn && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDetail?.()
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showUseBtn && useStatus === 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className={`use-btn ${getThemeClass()}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onUse?.()
|
||||
}}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 状态遮罩 */}
|
||||
{useStatus !== 0 && (
|
||||
<View className="gift-card-overlay">
|
||||
<View className="overlay-badge">
|
||||
{statusInfo.text}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCard
|
||||
169
src/components/GiftCardGuide.tsx
Normal file
169
src/components/GiftCardGuide.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Gift, QrCode, Voucher, Service } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface GiftCardGuideProps {
|
||||
/** 是否显示指南 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const GiftCardGuide: React.FC<GiftCardGuideProps> = ({
|
||||
visible,
|
||||
onClose
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
|
||||
const guideSteps = [
|
||||
{
|
||||
title: '如何获取礼品卡?',
|
||||
icon: <Gift size="24" className="text-yellow-500" />,
|
||||
content: [
|
||||
'1. 通过兑换码兑换礼品卡',
|
||||
'2. 扫描二维码快速兑换',
|
||||
'3. 参与活动获得礼品卡奖励',
|
||||
'4. 朋友赠送的礼品卡'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '如何兑换礼品卡?',
|
||||
icon: <QrCode size="24" className="text-blue-500" />,
|
||||
content: [
|
||||
'1. 点击"兑换"按钮进入兑换页面',
|
||||
'2. 输入礼品卡兑换码或扫码输入',
|
||||
'3. 验证兑换码有效性',
|
||||
'4. 确认兑换,礼品卡添加到账户'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '如何使用礼品卡?',
|
||||
icon: <Voucher size="24" className="text-green-500" />,
|
||||
content: [
|
||||
'1. 选择可用状态的礼品卡',
|
||||
'2. 点击"立即使用"按钮',
|
||||
'3. 填写使用信息(地址、备注等)',
|
||||
'4. 确认使用,完成礼品卡消费'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '礼品卡类型说明',
|
||||
icon: <Gift size="24" className="text-purple-500" />,
|
||||
content: [
|
||||
'🎁 实物礼品卡:需到指定地址领取商品',
|
||||
'💻 虚拟礼品卡:自动发放到账户余额',
|
||||
'🛎️ 服务礼品卡:联系客服预约服务',
|
||||
'⏰ 注意查看有效期,过期无法使用'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '常见问题解答',
|
||||
icon: <Service size="24" className="text-red-500" />,
|
||||
content: [
|
||||
'Q: 礼品卡可以转赠他人吗?',
|
||||
'A: 未使用的礼品卡可以通过分享功能转赠',
|
||||
'Q: 礼品卡过期了怎么办?',
|
||||
'A: 过期礼品卡无法使用,请及时关注有效期',
|
||||
'Q: 使用礼品卡后可以退款吗?',
|
||||
'A: 已使用的礼品卡不支持退款操作'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < guideSteps.length - 1) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSkip = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const currentGuide = guideSteps[currentStep]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable={false}
|
||||
style={{ width: '90%', maxWidth: '400px' }}
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="text-center mb-6">
|
||||
<View className="flex justify-center mb-3">
|
||||
{currentGuide.icon}
|
||||
</View>
|
||||
<Text className="text-xl font-bold text-gray-900">
|
||||
{currentGuide.title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className="mb-6">
|
||||
{currentGuide.content.map((item, index) => (
|
||||
<View key={index} className="mb-3">
|
||||
<Text className="text-gray-700 leading-relaxed">
|
||||
{item}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 进度指示器 */}
|
||||
<View className="flex justify-center mb-6">
|
||||
{guideSteps.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full mx-1 ${
|
||||
index === currentStep ? 'bg-yellow-500' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className="flex justify-between">
|
||||
<View className="flex gap-2">
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handleSkip}
|
||||
>
|
||||
跳过
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
>
|
||||
{currentStep === guideSteps.length - 1 ? '完成' : '下一步'}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardGuide
|
||||
173
src/components/GiftCardList.tsx
Normal file
173
src/components/GiftCardList.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React from 'react'
|
||||
import { View, ScrollView } from '@tarojs/components'
|
||||
import GiftCard, { GiftCardProps } from './GiftCard'
|
||||
|
||||
export interface GiftCardListProps {
|
||||
/** 礼品卡列表数据 */
|
||||
gifts: GiftCardProps[]
|
||||
/** 列表标题 */
|
||||
title?: string
|
||||
/** 布局方式:vertical-垂直布局 horizontal-水平滚动 grid-网格布局 */
|
||||
layout?: 'vertical' | 'horizontal' | 'grid'
|
||||
/** 网格列数(仅在grid布局时有效) */
|
||||
columns?: number
|
||||
/** 是否显示空状态 */
|
||||
showEmpty?: boolean
|
||||
/** 空状态文案 */
|
||||
emptyText?: string
|
||||
/** 礼品卡点击事件 */
|
||||
onGiftClick?: (gift: GiftCardProps, index: number) => void
|
||||
/** 礼品卡使用事件 */
|
||||
onGiftUse?: (gift: GiftCardProps, index: number) => void
|
||||
/** 礼品卡详情事件 */
|
||||
onGiftDetail?: (gift: GiftCardProps, index: number) => void
|
||||
}
|
||||
|
||||
const GiftCardList: React.FC<GiftCardListProps> = ({
|
||||
gifts = [],
|
||||
title,
|
||||
layout = 'vertical',
|
||||
columns = 2,
|
||||
showEmpty = true,
|
||||
emptyText = '暂无礼品卡',
|
||||
onGiftClick,
|
||||
onGiftUse,
|
||||
onGiftDetail
|
||||
}) => {
|
||||
const handleGiftClick = (gift: GiftCardProps, index: number) => {
|
||||
onGiftClick?.(gift, index)
|
||||
}
|
||||
|
||||
const handleGiftUse = (gift: GiftCardProps, index: number) => {
|
||||
onGiftUse?.(gift, index)
|
||||
}
|
||||
|
||||
const handleGiftDetail = (gift: GiftCardProps, index: number) => {
|
||||
onGiftDetail?.(gift, index)
|
||||
}
|
||||
|
||||
// 垂直布局
|
||||
if (layout === 'vertical') {
|
||||
return (
|
||||
<View className="p-4">
|
||||
{title && (
|
||||
<View className="font-semibold text-gray-800 mb-4 text-lg">{title}</View>
|
||||
)}
|
||||
|
||||
{gifts.length === 0 ? (
|
||||
showEmpty && (
|
||||
<View className="text-center py-16 px-5">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<View className="text-2xl">🎁</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-gray-500 text-base">{emptyText}</View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
gifts.map((gift, index) => (
|
||||
<GiftCard
|
||||
key={gift.id || index}
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 网格布局
|
||||
if (layout === 'grid') {
|
||||
return (
|
||||
<View className="p-4">
|
||||
{title && (
|
||||
<View className="font-semibold text-gray-800 mb-4 text-lg">{title}</View>
|
||||
)}
|
||||
|
||||
{gifts.length === 0 ? (
|
||||
showEmpty && (
|
||||
<View className="text-center py-16 px-5">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<View className="text-2xl">🎁</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-gray-500 text-base">{emptyText}</View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<View
|
||||
className="flex flex-wrap"
|
||||
style={{gap: '12px'}}
|
||||
>
|
||||
{gifts.map((gift, index) => (
|
||||
<View
|
||||
key={gift.id || index}
|
||||
className="w-full"
|
||||
style={{width: `calc(${100/columns}% - ${12*(columns-1)/columns}px)`}}
|
||||
>
|
||||
<GiftCard
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 水平滚动布局
|
||||
return (
|
||||
<View>
|
||||
{title && (
|
||||
<View className="font-semibold text-gray-800 mb-4 pl-4 text-lg">
|
||||
{title}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{gifts.length === 0 ? (
|
||||
showEmpty && (
|
||||
<View className="text-center py-16 px-5">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<View className="text-2xl">🎁</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-gray-500 text-base">{emptyText}</View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<ScrollView
|
||||
scrollX
|
||||
className="flex p-4 gap-3 overflow-x-auto"
|
||||
showScrollbar={false}
|
||||
style={{}}
|
||||
>
|
||||
{gifts.map((gift, index) => (
|
||||
<View
|
||||
key={gift.id || index}
|
||||
className="flex-shrink-0 w-80 mb-0"
|
||||
>
|
||||
<GiftCard
|
||||
{...gift}
|
||||
onClick={() => handleGiftClick(gift, index)}
|
||||
onUse={() => handleGiftUse(gift, index)}
|
||||
onDetail={() => handleGiftDetail(gift, index)}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardList
|
||||
227
src/components/GiftCardShare.tsx
Normal file
227
src/components/GiftCardShare.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Share, Wechat, QQ, Weibo, Link, Close, Gift } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface GiftCardShareProps {
|
||||
/** 是否显示分享弹窗 */
|
||||
visible: boolean
|
||||
/** 礼品卡信息 */
|
||||
giftCard: {
|
||||
id: number
|
||||
name: string
|
||||
type: number
|
||||
faceValue: string
|
||||
code?: string
|
||||
description?: string
|
||||
}
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const GiftCardShare: React.FC<GiftCardShareProps> = ({
|
||||
visible,
|
||||
giftCard,
|
||||
onClose
|
||||
}) => {
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (giftCard.type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
}
|
||||
}
|
||||
|
||||
// 生成分享文案
|
||||
const generateShareText = () => {
|
||||
const typeText = getTypeText()
|
||||
const valueText = `¥${giftCard.faceValue}`
|
||||
|
||||
return `🎁 ${giftCard.name}\n💰 面值 ${valueText}\n🏷️ ${typeText}\n${giftCard.description ? `📝 ${giftCard.description}\n` : ''}快来领取这份礼品卡吧!`
|
||||
}
|
||||
|
||||
// 生成分享链接
|
||||
const generateShareUrl = () => {
|
||||
// 这里应该是实际的分享链接,包含礼品卡ID等参数
|
||||
return `https://your-domain.com/gift/share?id=${giftCard.id}`
|
||||
}
|
||||
|
||||
// 微信分享
|
||||
const handleWechatShare = () => {
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
const handleCopyLink = () => {
|
||||
const shareUrl = generateShareUrl()
|
||||
const shareText = generateShareText()
|
||||
const fullText = `${shareText}\n\n${shareUrl}`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: fullText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制兑换码
|
||||
const handleCopyCode = () => {
|
||||
if (!giftCard.code) {
|
||||
Taro.showToast({
|
||||
title: '暂无兑换码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: giftCard.code,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '兑换码已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
onClose()
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存图片分享
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
// 这里可以生成礼品卡图片并保存到相册
|
||||
// 实际实现需要canvas绘制礼品卡图片
|
||||
Taro.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const shareOptions = [
|
||||
{
|
||||
icon: <Wechat size="32" className="text-green-500" />,
|
||||
label: '微信好友',
|
||||
onClick: handleWechatShare
|
||||
},
|
||||
{
|
||||
icon: <Link size="32" className="text-blue-500" />,
|
||||
label: '复制链接',
|
||||
onClick: handleCopyLink
|
||||
},
|
||||
{
|
||||
icon: <Gift size="32" className="text-purple-500" />,
|
||||
label: '复制兑换码',
|
||||
onClick: handleCopyCode,
|
||||
disabled: !giftCard.code
|
||||
},
|
||||
{
|
||||
icon: <Share size="32" className="text-orange-500" />,
|
||||
label: '保存图片',
|
||||
onClick: handleSaveImage
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
style={{ height: 'auto' }}
|
||||
round
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 头部 */}
|
||||
<View className="flex items-center justify-between mb-6">
|
||||
<Text className="text-lg font-semibold">分享礼品卡</Text>
|
||||
<View onClick={onClose}>
|
||||
<Close size="20" className="text-gray-500" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡预览 */}
|
||||
<View className="rounded-xl p-4 mb-6 text-white" style={{backgroundColor: '#fbbf24'}}>
|
||||
<Text className="text-xl font-bold mb-2">{giftCard.name}</Text>
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold">¥{giftCard.faceValue}</Text>
|
||||
<Text className="text-sm opacity-90">{getTypeText()}</Text>
|
||||
</View>
|
||||
<Gift size="24" />
|
||||
</View>
|
||||
{giftCard.code && (
|
||||
<View className="mt-3 p-2 bg-white bg-opacity-20 rounded">
|
||||
<Text className="text-xs opacity-80">兑换码</Text>
|
||||
<Text className="font-mono font-bold">{giftCard.code}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 分享选项 */}
|
||||
<View className="flex justify-between mb-4" style={{gap: '16px'}}>
|
||||
{shareOptions.map((option, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`flex-1 flex flex-col items-center py-4 bg-gray-50 rounded-lg ${
|
||||
option.disabled ? 'opacity-50' : ''
|
||||
}`}
|
||||
onClick={option.disabled ? undefined : option.onClick}
|
||||
>
|
||||
<View className="mb-2">{option.icon}</View>
|
||||
<Text className="text-sm text-gray-700">{option.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 分享文案预览 */}
|
||||
<View className="bg-gray-50 rounded-lg p-3">
|
||||
<Text className="text-xs text-gray-500 mb-2">分享文案预览:</Text>
|
||||
<Text className="text-sm text-gray-700 leading-relaxed">
|
||||
{generateShareText()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardShare
|
||||
72
src/components/GiftCardStats.tsx
Normal file
72
src/components/GiftCardStats.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Gift, Voucher, Clock } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface GiftCardStatsProps {
|
||||
/** 可用礼品卡数量 */
|
||||
availableCount: number
|
||||
/** 已使用礼品卡数量 */
|
||||
usedCount: number
|
||||
/** 已过期礼品卡数量 */
|
||||
expiredCount: number
|
||||
/** 礼品卡总价值 */
|
||||
totalValue?: number
|
||||
/** 点击统计项的回调 */
|
||||
onStatsClick?: (type: 'available' | 'used' | 'expired' | 'total') => void
|
||||
}
|
||||
|
||||
const GiftCardStats: React.FC<GiftCardStatsProps> = ({
|
||||
availableCount,
|
||||
usedCount,
|
||||
expiredCount,
|
||||
onStatsClick
|
||||
}) => {
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||
onStatsClick?.(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-white mx-4 my-3 rounded-xl p-3 shadow-sm">
|
||||
{/* 紧凑的统计卡片 - 2x2 网格 */}
|
||||
<View className="grid grid-cols-3 gap-2">
|
||||
{/* 可用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-2 bg-yellow-50 rounded-lg border border-yellow-200"
|
||||
onClick={() => handleStatsClick('available')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Gift size="16" className="text-yellow-600 mr-1" />
|
||||
<Text className="text-sm text-gray-600">可用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-yellow-600">{availableCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已使用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-2 bg-green-50 rounded-lg border border-green-200"
|
||||
onClick={() => handleStatsClick('used')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Voucher size="16" className="text-green-600 mr-1" />
|
||||
<Text className="text-sm text-gray-600">已使用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-green-600">{usedCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已过期礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-2 bg-gray-50 rounded-lg border border-gray-200"
|
||||
onClick={() => handleStatsClick('expired')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Clock size="16" className="text-gray-500 mr-1" />
|
||||
<Text className="text-sm text-gray-600">已过期</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-gray-500">{expiredCount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardStats
|
||||
87
src/components/GiftCardStatsMax.tsx
Normal file
87
src/components/GiftCardStatsMax.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Gift, Voucher, Clock, Star } from '@nutui/icons-react-taro'
|
||||
|
||||
export interface GiftCardStatsProps {
|
||||
/** 可用礼品卡数量 */
|
||||
availableCount: number
|
||||
/** 已使用礼品卡数量 */
|
||||
usedCount: number
|
||||
/** 已过期礼品卡数量 */
|
||||
expiredCount: number
|
||||
/** 礼品卡总价值 */
|
||||
totalValue?: number
|
||||
/** 点击统计项的回调 */
|
||||
onStatsClick?: (type: 'available' | 'used' | 'expired' | 'total') => void
|
||||
}
|
||||
|
||||
const GiftCardStats: React.FC<GiftCardStatsProps> = ({
|
||||
availableCount,
|
||||
usedCount,
|
||||
expiredCount,
|
||||
totalValue,
|
||||
onStatsClick
|
||||
}) => {
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||
onStatsClick?.(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-white mx-4 my-3 rounded-xl p-3 shadow-sm">
|
||||
{/* 紧凑的统计卡片 - 2x2 网格 */}
|
||||
<View className="grid grid-cols-2 gap-2">
|
||||
{/* 可用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-yellow-50 rounded-lg border border-yellow-200"
|
||||
onClick={() => handleStatsClick('available')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Gift size="20" className="text-yellow-600 mr-2" />
|
||||
<Text className="text-sm text-gray-600">可用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-yellow-600">{availableCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已使用礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200"
|
||||
onClick={() => handleStatsClick('used')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Voucher size="20" className="text-green-600 mr-2" />
|
||||
<Text className="text-sm text-gray-600">已使用</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-green-600">{usedCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已过期礼品卡 */}
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200"
|
||||
onClick={() => handleStatsClick('expired')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Clock size="20" className="text-gray-500 mr-2" />
|
||||
<Text className="text-sm text-gray-600">已过期</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-gray-500">{expiredCount}</Text>
|
||||
</View>
|
||||
|
||||
{/* 总价值 */}
|
||||
{totalValue !== undefined && (
|
||||
<View
|
||||
className="flex items-center justify-between p-3 bg-purple-50 rounded-lg border border-purple-200"
|
||||
onClick={() => handleStatsClick('total')}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<Star size="20" className="text-purple-600 mr-2" />
|
||||
<Text className="text-sm text-gray-600">总价值</Text>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-purple-600">¥{totalValue}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardStats
|
||||
@@ -1,75 +0,0 @@
|
||||
import { View, Button } from '@tarojs/components'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface EarningsCardProps {
|
||||
availableAmount?: number
|
||||
pendingAmount?: number
|
||||
onWithdraw?: () => void
|
||||
}
|
||||
|
||||
function EarningsCard({
|
||||
availableAmount = 0.00,
|
||||
pendingAmount = 0.00,
|
||||
onWithdraw
|
||||
}: EarningsCardProps) {
|
||||
|
||||
const handleWithdraw = () => {
|
||||
if (onWithdraw) {
|
||||
onWithdraw()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="mx-4 mb-4">
|
||||
<View
|
||||
className="rounded-2xl p-4 relative overflow-hidden"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #8B5CF6 0%, #A855F7 50%, #C084FC 100%)'
|
||||
}}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<View
|
||||
className="absolute -top-4 -right-4 w-20 h-20 rounded-full opacity-20"
|
||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
|
||||
/>
|
||||
<View
|
||||
className="absolute -bottom-6 -left-6 w-16 h-16 rounded-full opacity-10"
|
||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
|
||||
/>
|
||||
|
||||
<View className="relative z-10">
|
||||
{/* 可提现金额 */}
|
||||
<View className="mb-4">
|
||||
<View className="text-white text-base opacity-90 mb-1">
|
||||
可提现 {availableAmount.toFixed(2)} 元
|
||||
</View>
|
||||
<View className="text-white text-base opacity-90">
|
||||
待提现 {pendingAmount.toFixed(2)} 元
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 提现按钮 */}
|
||||
<View className="flex justify-end">
|
||||
<Button
|
||||
className="bg-white text-purple-600 px-6 py-2 rounded-full text-sm font-medium border-0"
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
color: '#8B5CF6',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
borderRadius: '20px',
|
||||
padding: '8px 24px',
|
||||
border: 'none'
|
||||
}}
|
||||
onClick={handleWithdraw}
|
||||
>
|
||||
去提现
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default EarningsCard
|
||||
@@ -1,102 +0,0 @@
|
||||
import { View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
interface MenuItem {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
color: string
|
||||
bgColor: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function FunctionMenu() {
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
id: 'withdraw-detail',
|
||||
title: '提现明细',
|
||||
icon: '💰',
|
||||
color: '#F59E0B',
|
||||
bgColor: '#FEF3C7',
|
||||
onClick: () => {
|
||||
Taro.navigateTo({
|
||||
url: '/dealer/withdraw/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'distribution-orders',
|
||||
title: '分销订单',
|
||||
icon: '📋',
|
||||
color: '#EF4444',
|
||||
bgColor: '#FEE2E2',
|
||||
onClick: () => {
|
||||
Taro.navigateTo({
|
||||
url: '/dealer/orders/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'my-team',
|
||||
title: '我的团队',
|
||||
icon: '👥',
|
||||
color: '#10B981',
|
||||
bgColor: '#D1FAE5',
|
||||
onClick: () => {
|
||||
Taro.navigateTo({
|
||||
url: '/dealer/team/index'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'promotion-qr',
|
||||
title: '推广二维码',
|
||||
icon: '📱',
|
||||
color: '#3B82F6',
|
||||
bgColor: '#DBEAFE',
|
||||
onClick: () => {
|
||||
Taro.navigateTo({
|
||||
url: '/dealer/qrcode/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
if (item.onClick) {
|
||||
item.onClick()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="mx-4 mb-4">
|
||||
<View className="bg-white rounded-2xl p-4 shadow-sm">
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
{menuItems.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className="flex flex-col items-center justify-center py-6 rounded-xl cursor-pointer"
|
||||
style={{ backgroundColor: item.bgColor }}
|
||||
onClick={() => handleMenuClick(item)}
|
||||
>
|
||||
<View
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center mb-2 text-2xl"
|
||||
style={{ backgroundColor: 'white' }}
|
||||
>
|
||||
{item.icon}
|
||||
</View>
|
||||
<View
|
||||
className="text-base font-medium"
|
||||
style={{ color: item.color }}
|
||||
>
|
||||
{item.title}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default FunctionMenu
|
||||
@@ -1,78 +0,0 @@
|
||||
import { View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
interface NavigationBarProps {
|
||||
title?: string
|
||||
showBack?: boolean
|
||||
showMore?: boolean
|
||||
onBack?: () => void
|
||||
onMore?: () => void
|
||||
}
|
||||
|
||||
function NavigationBar({
|
||||
title = '分销中心',
|
||||
showBack = true,
|
||||
showMore = true,
|
||||
onBack,
|
||||
onMore
|
||||
}: NavigationBarProps) {
|
||||
|
||||
const handleBack = () => {
|
||||
if (onBack) {
|
||||
onBack()
|
||||
} else {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
const handleMore = () => {
|
||||
if (onMore) {
|
||||
onMore()
|
||||
} else {
|
||||
Taro.showActionSheet({
|
||||
itemList: ['分享', '设置', '帮助']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="relative">
|
||||
{/* 状态栏占位 */}
|
||||
<View style={{ height: Taro.getSystemInfoSync().statusBarHeight + 'px' }} />
|
||||
|
||||
{/* 导航栏 */}
|
||||
<View className="flex items-center justify-between px-4 py-3 relative z-10">
|
||||
{/* 左侧返回按钮 */}
|
||||
<View className="w-8 h-8 flex items-center justify-center">
|
||||
{showBack && (
|
||||
<View
|
||||
className="w-6 h-6 flex items-center justify-center cursor-pointer"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<View className="text-white text-lg">‹</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 中间标题 */}
|
||||
<View className="flex-1 text-center">
|
||||
<View className="text-white text-xl font-medium">{title}</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧更多按钮 */}
|
||||
<View className="w-8 h-8 flex items-center justify-center">
|
||||
{showMore && (
|
||||
<View
|
||||
className="w-6 h-6 flex items-center justify-center cursor-pointer"
|
||||
onClick={handleMore}
|
||||
>
|
||||
<View className="text-white text-lg">⋯</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavigationBar
|
||||
@@ -1,211 +0,0 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {navigateTo} from '@tarojs/taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Button} from '@tarojs/components';
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {User} from "@/api/system/user/model";
|
||||
// import News from "./News";
|
||||
import {myPageBszxBm} from "@/api/bszx/bszxBm";
|
||||
import {listCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||
|
||||
const OrderIcon = () => {
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [bmLogs, setBmLogs] = useState<any>()
|
||||
const [navItems, setNavItems] = useState<any>([])
|
||||
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId
|
||||
},
|
||||
success: function (res) {
|
||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||||
setUserInfo(res.data.data.user)
|
||||
Taro.setStorageSync('Phone', res.data.data.user.phone)
|
||||
setIsLogin(true)
|
||||
Taro.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('登录失败!')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onLogin = (item: any, index: number) => {
|
||||
if(!isLogin){
|
||||
return navigateTo({url: `/pages/category/category?id=${item.navigationId}`})
|
||||
}else {
|
||||
// 报名链接
|
||||
if(index == 0){
|
||||
console.log(bmLogs,'bmLogs')
|
||||
if(bmLogs && bmLogs.length > 0){
|
||||
return navigateTo({url: `/bszx/bm-cert/bm-cert?id=${bmLogs[0].id}`})
|
||||
}else {
|
||||
navigateTo({url: `/user/profile/profile`})
|
||||
}
|
||||
}
|
||||
// 善款明细
|
||||
if(item.navigationId == 4119){
|
||||
return navigateTo({url: `/bszx/pay-record/pay-record`})
|
||||
}
|
||||
return navigateTo({url: `/pages/category/category?id=${item.navigationId}`})
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
// 读取栏目
|
||||
listCmsNavigation({parentId: 2828,hide: 0}).then(res => {
|
||||
console.log(res,'9999')
|
||||
setNavItems(res);
|
||||
})
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
setUserInfo({
|
||||
avatar,
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
console.log(userInfo, 'userInfo...')
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
});
|
||||
// 报名日志
|
||||
myPageBszxBm({limit: 1}).then(res => {
|
||||
if (res.list) {
|
||||
setBmLogs(res.list);
|
||||
}
|
||||
})
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
reload();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'my-3'}>
|
||||
<div className={'pt-4 bg-yellow-50 rounded-2xl'}
|
||||
style={{background: 'linear-gradient(to bottom, #ffffff, #ffffcc)'}}>
|
||||
<div className={'flex justify-between pb-2 px-1'}>
|
||||
{
|
||||
navItems.map((item, index) => (
|
||||
<div key={index} className={'text-center'}>
|
||||
{
|
||||
isLogin && !loading ?
|
||||
<div className={'flex flex-col justify-center items-center'} onClick={() => {
|
||||
onLogin(item, index)
|
||||
}}>
|
||||
<Image src={item.icon} height={28} width={28} lazyLoad={false}/>
|
||||
<div className={'mt-2'} style={{fontSize: '15px'}}>{item?.title}</div>
|
||||
</div>
|
||||
:
|
||||
<Button className={'text-white'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
||||
<div className={'flex flex-col justify-center items-center'}>
|
||||
<Image src={item.icon} height={28} width={28} lazyLoad={false}/>
|
||||
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>{item?.title}</div>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{/*<image src={'https://oss.wsdns.cn/20250224/18a2f3b807c94aac8a67af34e95534d6.jpeg'} className={'book'}>倡议书</image>*/}
|
||||
{/*<News id={categoryId}/>*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default OrderIcon
|
||||
@@ -1,135 +0,0 @@
|
||||
import {Avatar} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useEffect, useState} from "react";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {View} from '@tarojs/components'
|
||||
function UserCard() {
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [referrerName, setReferrerName] = useState<string>('平台')
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const reload = () => {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
setUserInfo({
|
||||
avatar,
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// 获取推荐人信息
|
||||
const referrer = data.nickname || '平台';
|
||||
setReferrerName(referrer)
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="mx-4 mb-4">
|
||||
<View className="bg-white rounded-2xl p-4 shadow-sm">
|
||||
<View className="flex items-center mb-3">
|
||||
<Avatar
|
||||
size="60"
|
||||
src={userInfo?.avatar || 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png'}
|
||||
shape="round"
|
||||
/>
|
||||
<View className="ml-3 flex-1">
|
||||
<View className="text-xl font-medium text-gray-800 mb-1">
|
||||
{IsLogin ? (userInfo?.nickname || '小程序用户') : '小程序.App.网站.系统开发-邓'}
|
||||
</View>
|
||||
<View className="text-base text-gray-500">
|
||||
推荐人:{referrerName}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="border-t border-gray-100 pt-3">
|
||||
<View className="flex justify-between items-center">
|
||||
<View className="text-base text-gray-600">
|
||||
已提现金额
|
||||
</View>
|
||||
<View className="text-xl font-medium text-gray-800">
|
||||
0.00元
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserCard;
|
||||
@@ -1,148 +0,0 @@
|
||||
import {Cell} from '@nutui/nutui-react-taro'
|
||||
import navTo from "@/utils/common";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask} from '@nutui/icons-react-taro'
|
||||
|
||||
const UserCell = () => {
|
||||
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
Taro.removeStorageSync('access_token')
|
||||
Taro.removeStorageSync('TenantId')
|
||||
Taro.removeStorageSync('UserId')
|
||||
Taro.removeStorageSync('userInfo')
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'px-4'}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
|
||||
}}
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<div>
|
||||
<span style={{ fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>开通会员</span>
|
||||
</div>
|
||||
<span className={'text-white opacity-80 pl-3'}>享优惠</span>
|
||||
</div>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
/>
|
||||
<Cell.Group divider={true} description={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<span style={{marginTop: '12px'}}>我的服务</span>
|
||||
</div>
|
||||
}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
style={{
|
||||
display: 'none'
|
||||
}}
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<LogisticsError size={16}/>
|
||||
<span className={'pl-3 text-sm'}>我的钱包</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/wallet/index', true)
|
||||
}}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Location size={16}/>
|
||||
<span className={'pl-3 text-sm'}>收货地址</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/address/index', true)
|
||||
}}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<ShieldCheck size={16}/>
|
||||
<span className={'pl-3 text-sm'}>实名认证</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/userVerify/index', true)
|
||||
}}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Ask size={16}/>
|
||||
<span className={'pl-3 text-sm'}>常见问题</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/help/index')
|
||||
}}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Tips size={16}/>
|
||||
<span className={'pl-3 text-sm'}>关于我们</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/about/index')
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
<Cell.Group divider={true} description={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<span style={{marginTop: '12px'}}>账号管理</span>
|
||||
</div>
|
||||
}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title="账号安全"
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/user/profile/profile', true)}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title="退出登录"
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={onLogout}
|
||||
/>
|
||||
</Cell.Group>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
@@ -1,102 +0,0 @@
|
||||
import {loginBySms} from "@/api/passport/login";
|
||||
import {useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Popup} from '@nutui/nutui-react-taro'
|
||||
import {UserParam} from "@/api/system/user/model";
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Form, Input} from '@nutui/nutui-react-taro'
|
||||
import {Copyright, Version} from "@/config/app";
|
||||
const UserFooter = () => {
|
||||
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
|
||||
const [clickNum, setClickNum] = useState<number>(0)
|
||||
const [FormData, setFormData] = useState<UserParam>(
|
||||
{
|
||||
phone: undefined,
|
||||
password: undefined
|
||||
}
|
||||
)
|
||||
|
||||
const onLoginByPhone = () => {
|
||||
setFormData({})
|
||||
setClickNum(clickNum + 1);
|
||||
if (clickNum > 10) {
|
||||
setOpenLoginByPhone(true);
|
||||
}
|
||||
}
|
||||
|
||||
const closeLoginByPhone = () => {
|
||||
setClickNum(0)
|
||||
setOpenLoginByPhone(false)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitByPhone = (values: any) => {
|
||||
loginBySms({
|
||||
phone: values.phone,
|
||||
code: values.code
|
||||
}).then(() => {
|
||||
setOpenLoginByPhone(false);
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
},1000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
||||
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
|
||||
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
||||
</div>
|
||||
|
||||
<Popup
|
||||
style={{width: '350px', padding: '10px'}}
|
||||
visible={openLoginByPhone}
|
||||
closeOnOverlayClick={false}
|
||||
closeable={true}
|
||||
onClose={closeLoginByPhone}
|
||||
>
|
||||
<Form
|
||||
style={{width: '350px',padding: '10px'}}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitByPhone(values)}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
label={'手机号码'}
|
||||
name="phone"
|
||||
required
|
||||
rules={[{message: '手机号码'}]}
|
||||
>
|
||||
<Input placeholder="请输入手机号码" maxLength={11} type="text"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'短信验证码'}
|
||||
name="code"
|
||||
required
|
||||
rules={[{message: '短信验证码'}]}
|
||||
>
|
||||
<Input placeholder="请输入短信验证码" maxLength={6} type="text"/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Popup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserFooter
|
||||
@@ -1,77 +0,0 @@
|
||||
// 分销中心页面样式
|
||||
.dealer-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #60A5FA 0%, #3B82F6 50%, #1D4ED8 100%);
|
||||
}
|
||||
|
||||
// 导航栏样式
|
||||
.navigation-bar {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
// 用户卡片样式
|
||||
.user-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// 收益卡片样式
|
||||
.earnings-card {
|
||||
background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 50%, #C084FC 100%);
|
||||
border-radius: 16px;
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 功能菜单样式
|
||||
.function-menu {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,37 @@
|
||||
import {useEffect} from 'react'
|
||||
import { View } from '@tarojs/components'
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Cell, Button } from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import NavigationBar from "./components/NavigationBar"
|
||||
import UserCard from "./components/UserCard"
|
||||
import EarningsCard from "./components/EarningsCard"
|
||||
import FunctionMenu from "./components/FunctionMenu"
|
||||
import './index.scss'
|
||||
function Index() {
|
||||
|
||||
useEffect(() => {
|
||||
// 设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '分销中心'
|
||||
})
|
||||
}, []);
|
||||
|
||||
const handleWithdraw = () => {
|
||||
Taro.showModal({
|
||||
title: '提现',
|
||||
content: '确定要进行提现操作吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
Taro.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
|
||||
const handleMore = () => {
|
||||
Taro.showActionSheet({
|
||||
itemList: ['分享给朋友', '客服咨询', '使用帮助'],
|
||||
success: (res) => {
|
||||
const actions = ['分享给朋友', '客服咨询', '使用帮助']
|
||||
Taro.showToast({
|
||||
title: actions[res.tapIndex],
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const DealerIndex: React.FC = () => {
|
||||
return (
|
||||
<View className="min-h-screen" style={{
|
||||
paddingTop: '20px',
|
||||
background: 'linear-gradient(180deg, #60A5FA 0%, #3B82F6 50%, #1D4ED8 100%)'
|
||||
}}>
|
||||
<UserCard />
|
||||
<EarningsCard onWithdraw={handleWithdraw} />
|
||||
<FunctionMenu />
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4">分销中心</Text>
|
||||
|
||||
<Cell.Group>
|
||||
<Cell
|
||||
title="我的团队"
|
||||
description="查看团队成员"
|
||||
extra={<Button size="small" onClick={() => Taro.navigateTo({url: '/dealer/team/index'})}>查看</Button>}
|
||||
/>
|
||||
<Cell
|
||||
title="我的订单"
|
||||
description="查看分销订单"
|
||||
extra={<Button size="small" onClick={() => Taro.navigateTo({url: '/dealer/orders/index'})}>查看</Button>}
|
||||
/>
|
||||
<Cell
|
||||
title="提现管理"
|
||||
description="申请提现"
|
||||
extra={<Button size="small" onClick={() => Taro.navigateTo({url: '/dealer/withdraw/index'})}>提现</Button>}
|
||||
/>
|
||||
<Cell
|
||||
title="推广二维码"
|
||||
description="生成推广码"
|
||||
extra={<Button size="small" onClick={() => Taro.navigateTo({url: '/dealer/qrcode/index'})}>生成</Button>}
|
||||
/>
|
||||
</Cell.Group>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
export default DealerIndex
|
||||
|
||||
3
src/dealer/orders/index.config.ts
Normal file
3
src/dealer/orders/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销订单'
|
||||
})
|
||||
@@ -1,145 +0,0 @@
|
||||
.distribution-orders-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: white;
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.orders-container {
|
||||
margin: 0 16px;
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.orders-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.order-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.order-no {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.order-content {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.product-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
flex: 1;
|
||||
|
||||
.product-name {
|
||||
font-size: 20px; // 对应 text-xl,主要标题
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.buyer-info {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.amount-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.order-amount {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.commission {
|
||||
font-size: 20px; // 对应 text-xl,重要数字
|
||||
font-weight: bold;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
color: #9ca3af;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { View, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Empty, Tabs, TabPane } from '@nutui/nutui-react-taro'
|
||||
import './index.scss'
|
||||
|
||||
interface DistributionOrder {
|
||||
id: string
|
||||
orderNo: string
|
||||
productName: string
|
||||
productImage: string
|
||||
buyerName: string
|
||||
orderAmount: number
|
||||
commission: number
|
||||
commissionRate: number
|
||||
status: 'pending' | 'confirmed' | 'settled'
|
||||
statusText: string
|
||||
createTime: string
|
||||
settleTime?: string
|
||||
}
|
||||
|
||||
function DistributionOrders() {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [orders, setOrders] = useState<DistributionOrder[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [stats, setStats] = useState({
|
||||
totalCommission: 0,
|
||||
pendingCommission: 0,
|
||||
settledCommission: 0,
|
||||
totalOrders: 0
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '分销订单'
|
||||
})
|
||||
loadOrders()
|
||||
}, [])
|
||||
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 模拟数据
|
||||
const mockData: DistributionOrder[] = [
|
||||
{
|
||||
id: '1',
|
||||
orderNo: 'DD202401150001',
|
||||
productName: '有机蔬菜礼盒装',
|
||||
productImage: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
buyerName: '张***',
|
||||
orderAmount: 299.00,
|
||||
commission: 29.90,
|
||||
commissionRate: 10,
|
||||
status: 'settled',
|
||||
statusText: '已结算',
|
||||
createTime: '2024-01-15 14:30:00',
|
||||
settleTime: '2024-01-16 10:00:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
orderNo: 'DD202401140002',
|
||||
productName: '新鲜水果组合',
|
||||
productImage: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
buyerName: '李***',
|
||||
orderAmount: 158.00,
|
||||
commission: 15.80,
|
||||
commissionRate: 10,
|
||||
status: 'confirmed',
|
||||
statusText: '已确认',
|
||||
createTime: '2024-01-14 09:20:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
orderNo: 'DD202401130003',
|
||||
productName: '农家土鸡蛋',
|
||||
productImage: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
buyerName: '王***',
|
||||
orderAmount: 88.00,
|
||||
commission: 8.80,
|
||||
commissionRate: 10,
|
||||
status: 'pending',
|
||||
statusText: '待确认',
|
||||
createTime: '2024-01-13 16:45:00'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算统计数据
|
||||
const totalCommission = mockData.reduce((sum, order) => sum + order.commission, 0)
|
||||
const pendingCommission = mockData.filter(o => o.status === 'pending').reduce((sum, order) => sum + order.commission, 0)
|
||||
const settledCommission = mockData.filter(o => o.status === 'settled').reduce((sum, order) => sum + order.commission, 0)
|
||||
|
||||
setTimeout(() => {
|
||||
setOrders(mockData)
|
||||
setStats({
|
||||
totalCommission,
|
||||
pendingCommission,
|
||||
settledCommission,
|
||||
totalOrders: mockData.length
|
||||
})
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error('加载分销订单失败:', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'settled':
|
||||
return '#10B981'
|
||||
case 'confirmed':
|
||||
return '#3B82F6'
|
||||
case 'pending':
|
||||
return '#F59E0B'
|
||||
default:
|
||||
return '#6B7280'
|
||||
}
|
||||
}
|
||||
|
||||
const getFilteredOrders = () => {
|
||||
switch (activeTab) {
|
||||
case '1':
|
||||
return orders.filter(order => order.status === 'pending')
|
||||
case '2':
|
||||
return orders.filter(order => order.status === 'confirmed')
|
||||
case '3':
|
||||
return orders.filter(order => order.status === 'settled')
|
||||
default:
|
||||
return orders
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrderClick = (order: DistributionOrder) => {
|
||||
Taro.showModal({
|
||||
title: '订单详情',
|
||||
content: `
|
||||
订单号:${order.orderNo}
|
||||
商品:${order.productName}
|
||||
购买人:${order.buyerName}
|
||||
订单金额:¥${order.orderAmount.toFixed(2)}
|
||||
佣金比例:${order.commissionRate}%
|
||||
佣金金额:¥${order.commission.toFixed(2)}
|
||||
下单时间:${order.createTime}
|
||||
${order.settleTime ? `结算时间:${order.settleTime}` : ''}
|
||||
`.trim(),
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="distribution-orders-page">
|
||||
<View className="loading-container">
|
||||
<View className="text-center text-gray-500">加载中...</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Cell, Empty } from '@nutui/nutui-react-taro'
|
||||
|
||||
const DealerOrders: React.FC = () => {
|
||||
return (
|
||||
<View className="distribution-orders-page">
|
||||
{/* 统计卡片 */}
|
||||
<View className="stats-card">
|
||||
<View className="stats-grid">
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">¥{stats.totalCommission.toFixed(2)}</View>
|
||||
<View className="stat-label">累计佣金</View>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">¥{stats.settledCommission.toFixed(2)}</View>
|
||||
<View className="stat-label">已结算</View>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">¥{stats.pendingCommission.toFixed(2)}</View>
|
||||
<View className="stat-label">待结算</View>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">{stats.totalOrders}</View>
|
||||
<View className="stat-label">订单数</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4">分销订单</Text>
|
||||
|
||||
{/* 订单列表 */}
|
||||
<View className="orders-container">
|
||||
<Tabs value={activeTab} onChange={(value) => setActiveTab(value)}>
|
||||
<TabPane title="全部" />
|
||||
<TabPane title="待确认" />
|
||||
<TabPane title="已确认" />
|
||||
<TabPane title="已结算" />
|
||||
</Tabs>
|
||||
|
||||
<View className="orders-list">
|
||||
{getFilteredOrders().length === 0 ? (
|
||||
<View className="empty-container">
|
||||
<Empty description="暂无订单" />
|
||||
</View>
|
||||
) : (
|
||||
getFilteredOrders().map((order) => (
|
||||
<View
|
||||
key={order.id}
|
||||
className="order-item"
|
||||
onClick={() => handleOrderClick(order)}
|
||||
>
|
||||
<View className="order-header">
|
||||
<View className="order-no">订单号:{order.orderNo}</View>
|
||||
<View
|
||||
className="order-status"
|
||||
style={{ color: getStatusColor(order.status) }}
|
||||
>
|
||||
{order.statusText}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="order-content">
|
||||
<Image className="product-image" src={order.productImage} />
|
||||
<View className="order-info">
|
||||
<View className="product-name">{order.productName}</View>
|
||||
<View className="buyer-info">购买人:{order.buyerName}</View>
|
||||
<View className="amount-info">
|
||||
<View className="order-amount">订单金额:¥{order.orderAmount.toFixed(2)}</View>
|
||||
<View className="commission">佣金:¥{order.commission.toFixed(2)}</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="order-time">下单时间:{order.createTime}</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Empty description="暂无分销订单" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DistributionOrders
|
||||
export default DealerOrders
|
||||
|
||||
3
src/dealer/qrcode/index.config.ts
Normal file
3
src/dealer/qrcode/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '推广二维码'
|
||||
})
|
||||
@@ -1,247 +0,0 @@
|
||||
.promotion-qrcode-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 16px;
|
||||
|
||||
.user-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
|
||||
.user-name {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.invite-code {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
background-color: #f3f4f6;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
.qrcode-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
|
||||
.qrcode-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-wrapper {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.qrcode-loading {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 12px;
|
||||
|
||||
.loading-text {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-image-container {
|
||||
.qrcode-placeholder {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 12px;
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.qr-pattern {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
position: relative;
|
||||
|
||||
.qr-corner {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid #1f2937;
|
||||
|
||||
&.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-dots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
padding: 40px 20px;
|
||||
|
||||
.qr-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1f2937;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-tip {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-info {
|
||||
.info-item {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
|
||||
.info-label {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
color: #1f2937;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: white;
|
||||
color: #3b82f6;
|
||||
border: 1px solid #3b82f6;
|
||||
}
|
||||
|
||||
&.tertiary {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usage-tips {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.tips-title {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tips-list {
|
||||
.tip-item {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 8px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +1,31 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { View, Canvas, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import './index.scss'
|
||||
|
||||
interface UserInfo {
|
||||
id: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
inviteCode: string
|
||||
}
|
||||
|
||||
function PromotionQRCode() {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo>({
|
||||
id: '12345',
|
||||
nickname: '分销达人',
|
||||
avatar: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
inviteCode: 'INV12345'
|
||||
})
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '推广二维码'
|
||||
})
|
||||
generateQRCode()
|
||||
}, [])
|
||||
|
||||
const generateQRCode = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 模拟生成二维码
|
||||
// 实际项目中应该调用后端API生成包含邀请码的二维码
|
||||
const inviteUrl = `https://your-domain.com/invite?code=${userInfo.inviteCode}`
|
||||
|
||||
// 这里使用一个模拟的二维码图片
|
||||
// 实际项目中可以使用二维码生成库或调用API
|
||||
const mockQRCode = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
|
||||
|
||||
setTimeout(() => {
|
||||
setQrCodeUrl(mockQRCode)
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
setLoading(false)
|
||||
Taro.showToast({
|
||||
title: '生成失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
if (!qrCodeUrl) {
|
||||
Taro.showToast({
|
||||
title: '二维码未生成',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 在实际项目中,这里应该将二维码保存到相册
|
||||
Taro.showModal({
|
||||
title: '保存二维码',
|
||||
content: '是否保存二维码到相册?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 实际保存逻辑
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('保存图片失败:', error)
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareQRCode = () => {
|
||||
Taro.showActionSheet({
|
||||
itemList: ['分享给朋友', '分享到朋友圈', '复制邀请链接'],
|
||||
success: (res) => {
|
||||
const actions = ['分享给朋友', '分享到朋友圈', '复制邀请链接']
|
||||
const action = actions[res.tapIndex]
|
||||
|
||||
if (action === '复制邀请链接') {
|
||||
const inviteUrl = `https://your-domain.com/invite?code=${userInfo.inviteCode}`
|
||||
Taro.setClipboardData({
|
||||
data: inviteUrl,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '链接已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: action,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRefreshQRCode = () => {
|
||||
Taro.showModal({
|
||||
title: '刷新二维码',
|
||||
content: '确定要重新生成二维码吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
generateQRCode()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
import React from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Cell, Button } from '@nutui/nutui-react-taro'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
return (
|
||||
<View className="promotion-qrcode-page">
|
||||
{/* 用户信息卡片 */}
|
||||
<View className="user-card">
|
||||
<Image className="user-avatar" src={userInfo.avatar} />
|
||||
<View className="user-info">
|
||||
<View className="user-name">{userInfo.nickname}</View>
|
||||
<View className="invite-code">邀请码:{userInfo.inviteCode}</View>
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4">推广二维码</Text>
|
||||
|
||||
<View className="text-center">
|
||||
<View className="bg-gray-100 w-48 h-48 mx-auto mb-4 flex items-center justify-center">
|
||||
<Text className="text-gray-500">二维码占位</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 二维码展示区域 */}
|
||||
<View className="qrcode-container">
|
||||
<View className="qrcode-card">
|
||||
<View className="qrcode-header">
|
||||
<View className="title">我的专属推广二维码</View>
|
||||
<View className="subtitle">扫码注册成为我的下级</View>
|
||||
</View>
|
||||
<Text className="text-sm text-gray-600 mb-4">
|
||||
扫描二维码或分享链接邀请好友
|
||||
</Text>
|
||||
|
||||
<View className="qrcode-wrapper">
|
||||
{loading ? (
|
||||
<View className="qrcode-loading">
|
||||
<View className="loading-text">生成中...</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="qrcode-image-container">
|
||||
{/* 实际项目中这里应该显示真实的二维码 */}
|
||||
<View className="qrcode-placeholder">
|
||||
<View className="qr-pattern">
|
||||
<View className="qr-corner top-left"></View>
|
||||
<View className="qr-corner top-right"></View>
|
||||
<View className="qr-corner bottom-left"></View>
|
||||
<View className="qr-dots">
|
||||
{Array.from({ length: 25 }).map((_, index) => (
|
||||
<View key={index} className="qr-dot"></View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="qrcode-tip">长按识别二维码</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="qrcode-info">
|
||||
<View className="info-item">
|
||||
<View className="info-label">邀请链接</View>
|
||||
<View className="info-value">https://your-domain.com/invite?code={userInfo.inviteCode}</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="action-buttons">
|
||||
<Button
|
||||
className="action-btn primary"
|
||||
onClick={handleSaveImage}
|
||||
disabled={loading}
|
||||
>
|
||||
保存到相册
|
||||
<Button type="primary" className="mb-2">
|
||||
保存二维码
|
||||
</Button>
|
||||
<Button
|
||||
className="action-btn secondary"
|
||||
onClick={handleShareQRCode}
|
||||
disabled={loading}
|
||||
>
|
||||
分享二维码
|
||||
</Button>
|
||||
<Button
|
||||
className="action-btn tertiary"
|
||||
onClick={handleRefreshQRCode}
|
||||
disabled={loading}
|
||||
>
|
||||
刷新二维码
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<View className="usage-tips">
|
||||
<View className="tips-title">使用说明</View>
|
||||
<View className="tips-list">
|
||||
<View className="tip-item">1. 分享二维码给好友,好友扫码注册成为您的下级</View>
|
||||
<View className="tip-item">2. 下级用户的消费订单将为您带来佣金收益</View>
|
||||
<View className="tip-item">3. 可以保存二维码图片或复制邀请链接进行推广</View>
|
||||
<View className="tip-item">4. 二维码长期有效,可重复使用</View>
|
||||
</View>
|
||||
<Button>
|
||||
分享链接
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromotionQRCode
|
||||
export default DealerQrcode
|
||||
|
||||
3
src/dealer/team/index.config.ts
Normal file
3
src/dealer/team/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
})
|
||||
@@ -1,176 +0,0 @@
|
||||
.my-team-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.team-stats {
|
||||
background: white;
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.level-stats {
|
||||
background: white;
|
||||
margin: 0 16px 16px;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.level-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.level-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.level-title {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.level-count {
|
||||
font-size: 20px; // 对应 text-xl
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.members-container {
|
||||
margin: 0 16px;
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.member-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
|
||||
.member-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.member-name {
|
||||
font-size: 20px; // 对应 text-xl,成员名称是重要信息
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.member-level {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
|
||||
&.level-1 {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
background-color: #10b981;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.stat {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.member-time {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.member-status {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
margin-left: 12px;
|
||||
|
||||
&.active {
|
||||
background-color: #d1fae5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
background-color: #fee2e2;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,244 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Avatar, Empty, Tabs, TabPane } from '@nutui/nutui-react-taro'
|
||||
import './index.scss'
|
||||
|
||||
interface TeamMember {
|
||||
id: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
joinTime: string
|
||||
level: number
|
||||
orderCount: number
|
||||
totalCommission: number
|
||||
status: 'active' | 'inactive'
|
||||
}
|
||||
|
||||
interface TeamStats {
|
||||
totalMembers: number
|
||||
activeMembers: number
|
||||
level1Members: number
|
||||
level2Members: number
|
||||
totalCommission: number
|
||||
monthCommission: number
|
||||
}
|
||||
|
||||
function MyTeam() {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [members, setMembers] = useState<TeamMember[]>([])
|
||||
const [stats, setStats] = useState<TeamStats>({
|
||||
totalMembers: 0,
|
||||
activeMembers: 0,
|
||||
level1Members: 0,
|
||||
level2Members: 0,
|
||||
totalCommission: 0,
|
||||
monthCommission: 0
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '我的团队'
|
||||
})
|
||||
loadTeamData()
|
||||
}, [])
|
||||
|
||||
const loadTeamData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 模拟数据
|
||||
const mockMembers: TeamMember[] = [
|
||||
{
|
||||
id: '1',
|
||||
nickname: '张小明',
|
||||
avatar: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
joinTime: '2024-01-15',
|
||||
level: 1,
|
||||
orderCount: 15,
|
||||
totalCommission: 150.50,
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
nickname: '李小红',
|
||||
avatar: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
joinTime: '2024-01-10',
|
||||
level: 1,
|
||||
orderCount: 8,
|
||||
totalCommission: 89.20,
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
nickname: '王小华',
|
||||
avatar: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
joinTime: '2024-01-08',
|
||||
level: 2,
|
||||
orderCount: 3,
|
||||
totalCommission: 25.80,
|
||||
status: 'inactive'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
nickname: '赵小刚',
|
||||
avatar: 'https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png',
|
||||
joinTime: '2024-01-05',
|
||||
level: 2,
|
||||
orderCount: 12,
|
||||
totalCommission: 98.60,
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算统计数据
|
||||
const totalMembers = mockMembers.length
|
||||
const activeMembers = mockMembers.filter(m => m.status === 'active').length
|
||||
const level1Members = mockMembers.filter(m => m.level === 1).length
|
||||
const level2Members = mockMembers.filter(m => m.level === 2).length
|
||||
const totalCommission = mockMembers.reduce((sum, m) => sum + m.totalCommission, 0)
|
||||
|
||||
setTimeout(() => {
|
||||
setMembers(mockMembers)
|
||||
setStats({
|
||||
totalMembers,
|
||||
activeMembers,
|
||||
level1Members,
|
||||
level2Members,
|
||||
totalCommission,
|
||||
monthCommission: totalCommission * 0.3 // 模拟本月佣金
|
||||
})
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error('加载团队数据失败:', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getFilteredMembers = () => {
|
||||
switch (activeTab) {
|
||||
case '1':
|
||||
return members.filter(member => member.level === 1)
|
||||
case '2':
|
||||
return members.filter(member => member.level === 2)
|
||||
case '3':
|
||||
return members.filter(member => member.status === 'active')
|
||||
default:
|
||||
return members
|
||||
}
|
||||
}
|
||||
|
||||
const handleMemberClick = (member: TeamMember) => {
|
||||
Taro.showModal({
|
||||
title: '成员详情',
|
||||
content: `
|
||||
昵称:${member.nickname}
|
||||
加入时间:${member.joinTime}
|
||||
等级:${member.level}级下线
|
||||
订单数量:${member.orderCount}
|
||||
累计佣金:¥${member.totalCommission.toFixed(2)}
|
||||
状态:${member.status === 'active' ? '活跃' : '不活跃'}
|
||||
`.trim(),
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="my-team-page">
|
||||
<View className="loading-container">
|
||||
<View className="text-center text-gray-500">加载中...</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty } from '@nutui/nutui-react-taro'
|
||||
|
||||
const DealerTeam: React.FC = () => {
|
||||
return (
|
||||
<View className="my-team-page">
|
||||
{/* 团队统计 */}
|
||||
<View className="team-stats">
|
||||
<View className="stats-grid">
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">{stats.totalMembers}</View>
|
||||
<View className="stat-label">团队总人数</View>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">{stats.activeMembers}</View>
|
||||
<View className="stat-label">活跃成员</View>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">¥{stats.totalCommission.toFixed(2)}</View>
|
||||
<View className="stat-label">累计佣金</View>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<View className="stat-value">¥{stats.monthCommission.toFixed(2)}</View>
|
||||
<View className="stat-label">本月佣金</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4">我的团队</Text>
|
||||
|
||||
{/* 等级统计 */}
|
||||
<View className="level-stats">
|
||||
<View className="level-item">
|
||||
<View className="level-info">
|
||||
<View className="level-title">一级下线</View>
|
||||
<View className="level-count">{stats.level1Members}人</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="level-item">
|
||||
<View className="level-info">
|
||||
<View className="level-title">二级下线</View>
|
||||
<View className="level-count">{stats.level2Members}人</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 成员列表 */}
|
||||
<View className="members-container">
|
||||
<Tabs value={activeTab} onChange={(value) => setActiveTab(value)}>
|
||||
<TabPane title="全部" />
|
||||
<TabPane title="一级" />
|
||||
<TabPane title="二级" />
|
||||
<TabPane title="活跃" />
|
||||
</Tabs>
|
||||
|
||||
<View className="members-list">
|
||||
{getFilteredMembers().length === 0 ? (
|
||||
<View className="empty-container">
|
||||
<Empty description="暂无团队成员" />
|
||||
</View>
|
||||
) : (
|
||||
getFilteredMembers().map((member) => (
|
||||
<View
|
||||
key={member.id}
|
||||
className="member-item"
|
||||
onClick={() => handleMemberClick(member)}
|
||||
>
|
||||
<Avatar size="50" src={member.avatar} shape="round" />
|
||||
<View className="member-info">
|
||||
<View className="member-header">
|
||||
<View className="member-name">{member.nickname}</View>
|
||||
<View className={`member-level level-${member.level}`}>
|
||||
{member.level}级
|
||||
</View>
|
||||
</View>
|
||||
<View className="member-stats">
|
||||
<View className="stat">订单:{member.orderCount}</View>
|
||||
<View className="stat">佣金:¥{member.totalCommission.toFixed(2)}</View>
|
||||
</View>
|
||||
<View className="member-time">加入时间:{member.joinTime}</View>
|
||||
</View>
|
||||
<View className={`member-status ${member.status}`}>
|
||||
{member.status === 'active' ? '活跃' : '不活跃'}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Empty description="暂无团队成员" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyTeam
|
||||
export default DealerTeam
|
||||
|
||||
3
src/dealer/withdraw/index.config.ts
Normal file
3
src/dealer/withdraw/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '提现申请'
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
.withdraw-detail-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 16px;
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
.record-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.amount {
|
||||
font-size: 20px; // 对应 text-xl,重要金额
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px; // 对应 text-sm
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.record-info {
|
||||
.time {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.remark {
|
||||
font-size: 16px; // 对应 text-base
|
||||
color: #ef4444;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background-color: #fef2f2;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,146 +1,35 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Empty } from '@nutui/nutui-react-taro'
|
||||
import './index.scss'
|
||||
|
||||
interface WithdrawRecord {
|
||||
id: string
|
||||
amount: number
|
||||
status: 'pending' | 'success' | 'failed'
|
||||
statusText: string
|
||||
createTime: string
|
||||
completeTime?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
function WithdrawDetail() {
|
||||
const [records, setRecords] = useState<WithdrawRecord[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '提现明细'
|
||||
})
|
||||
loadWithdrawRecords()
|
||||
}, [])
|
||||
|
||||
const loadWithdrawRecords = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 模拟数据,实际应该调用API
|
||||
const mockData: WithdrawRecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
amount: 100.00,
|
||||
status: 'success',
|
||||
statusText: '提现成功',
|
||||
createTime: '2024-01-15 14:30:00',
|
||||
completeTime: '2024-01-15 16:45:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
amount: 50.00,
|
||||
status: 'pending',
|
||||
statusText: '处理中',
|
||||
createTime: '2024-01-10 09:20:00'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
amount: 200.00,
|
||||
status: 'failed',
|
||||
statusText: '提现失败',
|
||||
createTime: '2024-01-05 11:15:00',
|
||||
remark: '银行卡信息有误'
|
||||
}
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
setRecords(mockData)
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error('加载提现记录失败:', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return '#10B981'
|
||||
case 'pending':
|
||||
return '#F59E0B'
|
||||
case 'failed':
|
||||
return '#EF4444'
|
||||
default:
|
||||
return '#6B7280'
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecordClick = (record: WithdrawRecord) => {
|
||||
const content = `
|
||||
提现金额:¥${record.amount.toFixed(2)}
|
||||
申请时间:${record.createTime}
|
||||
${record.completeTime ? `完成时间:${record.completeTime}` : ''}
|
||||
${record.remark ? `备注:${record.remark}` : ''}
|
||||
`.trim()
|
||||
|
||||
Taro.showModal({
|
||||
title: '提现详情',
|
||||
content,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="withdraw-detail-page">
|
||||
<View className="loading-container">
|
||||
<View className="text-center text-gray-500">加载中...</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Cell, Button, Form, Input } from '@nutui/nutui-react-taro'
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
return (
|
||||
<View className="withdraw-detail-page">
|
||||
{records.length === 0 ? (
|
||||
<View className="empty-container">
|
||||
<Empty description="暂无提现记录" />
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-bold mb-4">提现申请</Text>
|
||||
|
||||
<Form>
|
||||
<Cell.Group>
|
||||
<Form.Item name="amount" label="提现金额" required>
|
||||
<Input placeholder="请输入提现金额" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="account" label="提现账户" required>
|
||||
<Input placeholder="请输入提现账户" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input placeholder="请输入备注信息" />
|
||||
</Form.Item>
|
||||
</Cell.Group>
|
||||
|
||||
<View className="mt-4">
|
||||
<Button block type="primary">
|
||||
申请提现
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View className="records-list">
|
||||
{records.map((record) => (
|
||||
<View
|
||||
key={record.id}
|
||||
className="record-item"
|
||||
onClick={() => handleRecordClick(record)}
|
||||
>
|
||||
<View className="record-header">
|
||||
<View className="amount">¥{record.amount.toFixed(2)}</View>
|
||||
<View
|
||||
className="status"
|
||||
style={{ color: getStatusColor(record.status) }}
|
||||
>
|
||||
{record.statusText}
|
||||
</View>
|
||||
</View>
|
||||
<View className="record-info">
|
||||
<View className="time">申请时间:{record.createTime}</View>
|
||||
{record.completeTime && (
|
||||
<View className="time">完成时间:{record.completeTime}</View>
|
||||
)}
|
||||
{record.remark && (
|
||||
<View className="remark">备注:{record.remark}</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</Form>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default WithdrawDetail
|
||||
export default DealerWithdraw
|
||||
|
||||
@@ -15,6 +15,7 @@ function UserCard() {
|
||||
const [roleName, setRoleName] = useState<string>('注册用户')
|
||||
const [couponCount, setCouponCount] = useState(0)
|
||||
const [pointsCount, setPointsCount] = useState(0)
|
||||
const [giftCount, setGiftCount] = useState(0)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,6 +53,11 @@ function UserCard() {
|
||||
.catch((error: any) => {
|
||||
console.error('Points stats error:', error)
|
||||
})
|
||||
// 加载礼品劵数量
|
||||
setGiftCount(0)
|
||||
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
|
||||
// setGiftCount(res.count || 0)
|
||||
// })
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
@@ -85,7 +91,7 @@ function UserCard() {
|
||||
}
|
||||
// 判断身份
|
||||
const roleName = Taro.getStorageSync('RoleName');
|
||||
if(roleName){
|
||||
if (roleName) {
|
||||
setRoleName(roleName)
|
||||
}
|
||||
}
|
||||
@@ -153,7 +159,7 @@ function UserCard() {
|
||||
TenantId
|
||||
},
|
||||
success: function (res) {
|
||||
if(res.data.code == 1){
|
||||
if (res.data.code == 1) {
|
||||
Taro.showToast({
|
||||
title: res.data.message,
|
||||
icon: 'error',
|
||||
@@ -216,15 +222,23 @@ function UserCard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex justify-around mt-1'}>
|
||||
<div className={'item flex justify-center flex-col items-center'} onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||
<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 || '0.00'}</span>
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'} onClick={() => navTo('/user/coupon/coupon', true)}>
|
||||
<div className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/coupon/index', true)}>
|
||||
<span className={'text-sm text-gray-500'}>优惠券</span>
|
||||
<span className={'text-xl'}>{couponCount}</span>
|
||||
</div>
|
||||
<div className={'item flex justify-center flex-col items-center'} onClick={() => navTo('/user/points/points', true)}>
|
||||
<div className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/gift/index', true)}>
|
||||
<span className={'text-sm text-gray-500'}>礼品卡</span>
|
||||
<span className={'text-xl'}>{giftCount}</span>
|
||||
</div>
|
||||
<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'}>{pointsCount}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopArticle} from "@/api/shop/shopArticle/model";
|
||||
import {pageShopArticle, removeShopArticle} from "@/api/shop/shopArticle";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const ShopArticleManage = () => {
|
||||
const [list, setList] = useState<ShopArticle[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
const res = await pageShopArticle({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: searchValue
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
setTotal(res.count || 0)
|
||||
|
||||
// 判断是否还有更多数据
|
||||
setHasMore(res.list.length === 10) // 如果返回的数据等于limit,说明可能还有更多
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2) // 刷新后下一页是第2页
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文章失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取文章失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchValue(value)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await reload(true)
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 删除文章
|
||||
const handleDelete = async (id?: number) => {
|
||||
Taro.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这篇文章吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await removeShopArticle(id)
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload(true);
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
const handleEdit = (item: ShopArticle) => {
|
||||
Taro.navigateTo({
|
||||
url: `/shop/shopArticle/add?id=${item.articleId}`
|
||||
});
|
||||
}
|
||||
|
||||
// 查看文章详情
|
||||
const handleView = (item: ShopArticle) => {
|
||||
// 这里可以跳转到文章详情页面
|
||||
Taro.navigateTo({
|
||||
url: `/cms/detail/index?id=${item.articleId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusTag = (status?: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return <Tag type="success">已发布</Tag>
|
||||
case 1:
|
||||
return <Tag type="warning">待审核</Tag>
|
||||
case 2:
|
||||
return <Tag type="danger">已驳回</Tag>
|
||||
case 3:
|
||||
return <Tag type="danger">违规内容</Tag>
|
||||
default:
|
||||
return <Tag>未知</Tag>
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false) // 不刷新,追加数据
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 搜索栏 */}
|
||||
<View className="py-2">
|
||||
<SearchBar
|
||||
placeholder="搜索关键词"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{total > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500">
|
||||
共找到 {total} 篇文章
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 文章列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
pullDistance={60}
|
||||
headHeight={60}
|
||||
>
|
||||
<View className="px-4" style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="article-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 200px)'}}>
|
||||
<Empty
|
||||
description="暂无文章数据"
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="article-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
{list.map((item, index) => (
|
||||
<CellGroup key={item.articleId || index} className="mb-4">
|
||||
<Cell>
|
||||
<View className="flex flex-col gap-3 w-full">
|
||||
{/* 文章标题和状态 */}
|
||||
<View className="flex justify-between items-start">
|
||||
<View className="flex-1 pr-2">
|
||||
<View className="text-lg font-bold text-gray-900 line-clamp-2">
|
||||
{item.title}
|
||||
</View>
|
||||
</View>
|
||||
{getStatusTag(item.status)}
|
||||
</View>
|
||||
|
||||
{/* 文章概述 */}
|
||||
{item.overview && (
|
||||
<View className="text-sm text-gray-600 line-clamp-2">
|
||||
{item.overview}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 文章信息 */}
|
||||
<View className="flex justify-between items-center text-xs text-gray-500">
|
||||
<View className="flex items-center gap-4">
|
||||
<View>阅读: {item.actualViews || 0}</View>
|
||||
{item.price && <View>价格: ¥{item.price}</View>}
|
||||
<View>创建: {dayjs(item.createTime).format('MM-DD HH:mm')}</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="flex justify-end gap-2 pt-2 border-t border-gray-100">
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Eye/>}
|
||||
onClick={() => handleView(item)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Edit/>}
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="danger"
|
||||
fill="outline"
|
||||
icon={<Del/>}
|
||||
onClick={() => handleDelete(item.articleId)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
</CellGroup>
|
||||
))}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton
|
||||
text="发布文章"
|
||||
icon={<Edit />}
|
||||
onClick={() => Taro.navigateTo({url: '/shop/shopArticle/add'})}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ShopArticleManage;
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品文章管理',
|
||||
navigationBarTitleText: '新增收货地址',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
323
src/user/coupon/add.tsx
Normal file
323
src/user/coupon/add.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, Loading, CellGroup, Input, TextArea, Form, Switch, InputNumber, Radio, Image} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Upload as UploadIcon} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopArticle} from "@/api/shop/shopArticle/model";
|
||||
import {getShopArticle, addShopArticle, updateShopArticle} from "@/api/shop/shopArticle";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
|
||||
const AddShopArticle = () => {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [formData, setFormData] = useState<ShopArticle>({
|
||||
type: 0, // 默认常规文章
|
||||
status: 0, // 默认已发布
|
||||
permission: 0, // 默认所有人可见
|
||||
recommend: 0, // 默认不推荐
|
||||
showType: 10, // 默认小图展示
|
||||
virtualViews: 0, // 默认虚拟阅读量
|
||||
actualViews: 0, // 默认实际阅读量
|
||||
sortNumber: 0 // 默认排序
|
||||
})
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 判断是编辑还是新增模式
|
||||
const isEditMode = !!params.id
|
||||
const articleId = params.id ? Number(params.id) : undefined
|
||||
|
||||
// 文章类型选项
|
||||
const typeOptions = [
|
||||
{ text: '常规文章', value: 0 },
|
||||
{ text: '视频文章', value: 1 }
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ text: '已发布', value: 0 },
|
||||
{ text: '待审核', value: 1 },
|
||||
{ text: '已驳回', value: 2 },
|
||||
{ text: '违规内容', value: 3 }
|
||||
]
|
||||
|
||||
// 可见性选项
|
||||
const permissionOptions = [
|
||||
{ text: '所有人可见', value: 0 },
|
||||
{ text: '登录可见', value: 1 },
|
||||
{ text: '密码可见', value: 2 }
|
||||
]
|
||||
|
||||
// 显示方式选项
|
||||
const showTypeOptions = [
|
||||
{ text: '小图展示', value: 10 },
|
||||
{ text: '大图展示', value: 20 }
|
||||
]
|
||||
|
||||
const reload = async () => {
|
||||
// 如果是编辑模式,加载文章数据
|
||||
if (isEditMode && articleId) {
|
||||
try {
|
||||
const article = await getShopArticle(articleId)
|
||||
setFormData(article)
|
||||
// 更新表单值
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue(article)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文章失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载文章失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传处理
|
||||
const handleImageUpload = async () => {
|
||||
try {
|
||||
const res = await Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
});
|
||||
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
// 这里应该调用上传接口,暂时使用本地路径
|
||||
const imagePath = res.tempFilePaths[0];
|
||||
setFormData({
|
||||
...formData,
|
||||
image: imagePath
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '图片选择成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '图片选择失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...formData,
|
||||
...values,
|
||||
};
|
||||
|
||||
// 如果是编辑模式,添加id
|
||||
if (isEditMode && articleId) {
|
||||
submitData.articleId = articleId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopArticle(submitData);
|
||||
} else {
|
||||
await addShopArticle(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}失败`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 动态设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: isEditMode ? '编辑文章' : '新增文章'
|
||||
});
|
||||
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [isEditMode]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={formData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<CellGroup title="基本信息">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="文章标题"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入文章标题' }]}
|
||||
initialValue={formData.title}
|
||||
>
|
||||
<Input placeholder="请输入文章标题" maxLength={100}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="overview" label="文章概述" initialValue={formData.overview}>
|
||||
<TextArea placeholder="请输入文章概述,用于列表展示" maxLength={200} rows={3}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="detail"
|
||||
label="文章内容"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入文章内容' }]}
|
||||
initialValue={formData.detail}
|
||||
>
|
||||
<TextArea placeholder="请输入文章内容" maxLength={10000} rows={8}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="author" label="作者" initialValue={formData.author}>
|
||||
<Input placeholder="请输入作者名称" maxLength={50}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="source" label="来源" initialValue={formData.source}>
|
||||
<Input placeholder="请输入文章来源" maxLength={100}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 文章设置 */}
|
||||
<CellGroup title="文章设置">
|
||||
<Form.Item name="type" label="文章类型" initialValue={formData.type}>
|
||||
<Radio.Group direction="horizontal" value={formData.type}>
|
||||
{typeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="status" label="发布状态" initialValue={formData.status}>
|
||||
<Radio.Group direction="horizontal" value={formData.status}>
|
||||
{statusOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="permission" label="可见性" initialValue={formData.permission}>
|
||||
<Radio.Group direction="horizontal" value={formData.permission}>
|
||||
{permissionOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="showType" label="显示方式" initialValue={formData.showType}>
|
||||
<Radio.Group direction="horizontal" value={formData.showType}>
|
||||
{showTypeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 高级设置 */}
|
||||
<CellGroup title="高级设置">
|
||||
<Form.Item name="recommend" label="推荐文章" initialValue={formData.recommend}>
|
||||
<Switch
|
||||
checked={formData.recommend === 1}
|
||||
onChange={(checked) =>
|
||||
setFormData({...formData, recommend: checked ? 1 : 0})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="price" label="付费金额" initialValue={formData.price}>
|
||||
<Input placeholder="0.00" type="number"/>
|
||||
<View className="text-xs text-gray-500 mt-1">单位:元</View>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="virtualViews" label="虚拟阅读量" initialValue={formData.virtualViews}>
|
||||
<InputNumber min={0} defaultValue={formData.virtualViews || 0}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="actualViews" label="实际阅读量" initialValue={formData.actualViews}>
|
||||
<InputNumber min={0} defaultValue={formData.actualViews || 0}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="sortNumber" label="排序" initialValue={formData.sortNumber}>
|
||||
<InputNumber min={0} defaultValue={formData.sortNumber || 0}/>
|
||||
<View className="text-xs text-gray-500 mt-1">数字越小越靠前</View>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="tags" label="标签" initialValue={formData.tags}>
|
||||
<Input placeholder="请输入标签,多个标签用逗号分隔" maxLength={200}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="topic" label="话题" initialValue={formData.topic}>
|
||||
<Input placeholder="请输入话题" maxLength={100}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 图片上传 */}
|
||||
<CellGroup title="文章图片">
|
||||
<Form.Item name="image" label="封面图片" initialValue={formData.image}>
|
||||
<View className="flex items-center gap-3">
|
||||
{formData.image && (
|
||||
<Image
|
||||
src={formData.image}
|
||||
width="80"
|
||||
height="80"
|
||||
radius="8"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
fill="outline"
|
||||
icon={<UploadIcon />}
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
{formData.image ? '更换图片' : '上传图片'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<FixedButton text={isEditMode ? '更新文章' : '发布文章'} onClick={submitSucceed} icon={<Edit />} />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddShopArticle;
|
||||
6
src/user/coupon/detail.config.ts
Normal file
6
src/user/coupon/detail.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '优惠券详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
259
src/user/coupon/detail.tsx
Normal file
259
src/user/coupon/detail.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import {useState, useEffect} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, ConfigProvider, Tag, Divider} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft, Gift, Clock, CartCheck, Share} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
|
||||
import {getShopCoupon} from "@/api/shop/shopCoupon";
|
||||
import CouponShare from "@/components/CouponShare";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const CouponDetail = () => {
|
||||
const router = useRouter()
|
||||
const [coupon, setCoupon] = useState<ShopCoupon | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showShare, setShowShare] = useState(false)
|
||||
const couponId = router.params.id
|
||||
|
||||
useEffect(() => {
|
||||
if (couponId) {
|
||||
loadCouponDetail()
|
||||
}
|
||||
}, [couponId])
|
||||
|
||||
const loadCouponDetail = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await getShopCoupon(Number(couponId))
|
||||
setCoupon(data)
|
||||
} catch (error) {
|
||||
console.error('获取优惠券详情失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取优惠券详情失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠券类型文本
|
||||
const getCouponTypeText = (type?: number) => {
|
||||
switch (type) {
|
||||
case 10: return '满减券'
|
||||
case 20: return '折扣券'
|
||||
case 30: return '免费券'
|
||||
default: return '优惠券'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠券金额显示
|
||||
const getCouponAmountDisplay = () => {
|
||||
if (!coupon) return ''
|
||||
|
||||
switch (coupon.type) {
|
||||
case 10: // 满减券
|
||||
return `¥${coupon.reducePrice}`
|
||||
case 20: // 折扣券
|
||||
return `${coupon.discount}折`
|
||||
case 30: // 免费券
|
||||
return '免费'
|
||||
default:
|
||||
return `¥${coupon.reducePrice || 0}`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取使用条件文本
|
||||
const getConditionText = () => {
|
||||
if (!coupon) return ''
|
||||
|
||||
if (coupon.type === 30) return '无门槛使用'
|
||||
if (coupon.minPrice && parseFloat(coupon.minPrice) > 0) {
|
||||
return `满${coupon.minPrice}元可用`
|
||||
}
|
||||
return '无门槛使用'
|
||||
}
|
||||
|
||||
// 获取有效期文本
|
||||
const getValidityText = () => {
|
||||
if (!coupon) return ''
|
||||
|
||||
if (coupon.expireType === 10) {
|
||||
return `领取后${coupon.expireDay}天内有效`
|
||||
} else {
|
||||
return `${dayjs(coupon.startTime).format('YYYY年MM月DD日')} 至 ${dayjs(coupon.endTime).format('YYYY年MM月DD日')}`
|
||||
}
|
||||
}
|
||||
|
||||
// 获取适用范围文本
|
||||
const getApplyRangeText = () => {
|
||||
if (!coupon) return ''
|
||||
|
||||
switch (coupon.applyRange) {
|
||||
case 10: return '全部商品'
|
||||
case 20: return '指定商品'
|
||||
case 30: return '指定分类'
|
||||
default: return '全部商品'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠券状态
|
||||
const getCouponStatus = () => {
|
||||
if (!coupon) return { status: 0, text: '未知', color: 'default' }
|
||||
|
||||
if (coupon.isExpire === 1) {
|
||||
return { status: 2, text: '已过期', color: 'danger' }
|
||||
} else if (coupon.status === 1) {
|
||||
return { status: 1, text: '已使用', color: 'warning' }
|
||||
} else {
|
||||
return { status: 0, text: '可使用', color: 'success' }
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优惠券
|
||||
const handleUseCoupon = () => {
|
||||
if (!coupon) return
|
||||
|
||||
Taro.showModal({
|
||||
title: '使用优惠券',
|
||||
content: `确定要使用"${coupon.name}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到商品页面或购物车页面
|
||||
Taro.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex justify-center items-center h-screen">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (!coupon) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex flex-col justify-center items-center h-screen">
|
||||
<Text className="text-gray-500 mb-4">优惠券不存在</Text>
|
||||
<Button onClick={handleBack}>返回</Button>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = getCouponStatus()
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 自定义导航栏 */}
|
||||
<View className="flex items-center justify-between p-4 bg-white border-b border-gray-100">
|
||||
<View className="flex items-center" onClick={handleBack}>
|
||||
<ArrowLeft size="20" />
|
||||
<Text className="ml-2 text-lg">优惠券详情</Text>
|
||||
</View>
|
||||
<View className="flex items-center gap-3">
|
||||
<View onClick={() => setShowShare(true)}>
|
||||
<Share size="20" className="text-gray-600" />
|
||||
</View>
|
||||
<Tag type={statusInfo.color as any}>{statusInfo.text}</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 优惠券卡片 */}
|
||||
<View className="m-4 p-6 bg-gradient-to-r from-red-400 to-red-500 rounded-2xl text-white">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<View>
|
||||
<Text className="text-4xl font-bold">{getCouponAmountDisplay()}</Text>
|
||||
<Text className="text-lg opacity-90 mt-1">{getCouponTypeText(coupon.type)}</Text>
|
||||
</View>
|
||||
<Gift size="40" />
|
||||
</View>
|
||||
|
||||
<Text className="text-xl font-semibold mb-2">{coupon.name}</Text>
|
||||
<Text className="text-base opacity-90">{getConditionText()}</Text>
|
||||
</View>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<View className="bg-white mx-4 rounded-xl p-4">
|
||||
<Text className="text-lg font-semibold mb-4">使用说明</Text>
|
||||
|
||||
<View className="space-y-3">
|
||||
<View className="flex items-center">
|
||||
<Clock size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">有效期</Text>
|
||||
<Text className="text-gray-900">{getValidityText()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View className="flex items-center">
|
||||
<CartCheck size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">适用范围</Text>
|
||||
<Text className="text-gray-900">{getApplyRangeText()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{coupon.description && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm mb-2">使用说明</Text>
|
||||
<Text className="text-gray-900 leading-relaxed">{coupon.description}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
{statusInfo.status === 0 && (
|
||||
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={handleUseCoupon}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 分享弹窗 */}
|
||||
{coupon && (
|
||||
<CouponShare
|
||||
visible={showShare}
|
||||
coupon={{
|
||||
id: coupon.id || 0,
|
||||
name: coupon.name || '',
|
||||
type: coupon.type || 10,
|
||||
amount: coupon.type === 10 ? coupon.reducePrice || '0' :
|
||||
coupon.type === 20 ? coupon.discount?.toString() || '0' : '0',
|
||||
minAmount: coupon.minPrice,
|
||||
description: coupon.description
|
||||
}}
|
||||
onClose={() => setShowShare(false)}
|
||||
/>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CouponDetail;
|
||||
5
src/user/coupon/index.config.ts
Normal file
5
src/user/coupon/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的优惠券',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
470
src/user/coupon/index.tsx
Normal file
470
src/user/coupon/index.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Empty, ConfigProvider, SearchBar, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro'
|
||||
import {Gift, Search, Plus, Filter} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
|
||||
import {pageShopCoupon} from "@/api/shop/shopCoupon";
|
||||
import CouponList from "@/components/CouponList";
|
||||
import CouponStats from "@/components/CouponStats";
|
||||
import CouponGuide from "@/components/CouponGuide";
|
||||
import CouponFilter from "@/components/CouponFilter";
|
||||
import CouponExpireNotice, {ExpiringSoon} from "@/components/CouponExpireNotice";
|
||||
import {CouponCardProps} from "@/components/CouponCard";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const CouponManage = () => {
|
||||
const [list, setList] = useState<ShopCoupon[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [activeTab, setActiveTab] = useState('0') // 0-可用 1-已使用 2-已过期
|
||||
const [stats, setStats] = useState({
|
||||
available: 0,
|
||||
used: 0,
|
||||
expired: 0
|
||||
})
|
||||
const [showGuide, setShowGuide] = useState(false)
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
const [showExpireNotice, setShowExpireNotice] = useState(false)
|
||||
const [expiringSoonCoupons, setExpiringSoonCoupons] = useState<ExpiringSoon[]>([])
|
||||
const [filters, setFilters] = useState({
|
||||
type: [] as number[],
|
||||
minAmount: undefined as number | undefined,
|
||||
sortBy: 'createTime' as 'createTime' | 'amount' | 'expireTime',
|
||||
sortOrder: 'desc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
// 获取优惠券状态过滤条件
|
||||
const getStatusFilter = () => {
|
||||
switch (activeTab) {
|
||||
case '0': // 可用
|
||||
return { status: 0, isExpire: 0 }
|
||||
case '1': // 已使用
|
||||
return { status: 1 }
|
||||
case '2': // 已过期
|
||||
return { isExpire: 1 }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
const statusFilter = getStatusFilter()
|
||||
const res = await pageShopCoupon({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: searchValue,
|
||||
...statusFilter,
|
||||
// 应用筛选条件
|
||||
...(filters.type.length > 0 && { type: filters.type[0] }),
|
||||
...(filters.minAmount && { minAmount: filters.minAmount }),
|
||||
sortBy: filters.sortBy,
|
||||
sortOrder: filters.sortOrder
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
setTotal(res.count || 0)
|
||||
|
||||
// 判断是否还有更多数据
|
||||
setHasMore(res.list.length === 10) // 如果返回的数据等于limit,说明可能还有更多
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2) // 刷新后下一页是第2页
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠券失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取优惠券失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchValue(value)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await reload(true)
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value)
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
// 延迟执行reload,确保状态更新完成
|
||||
setTimeout(() => {
|
||||
reload(true)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 转换优惠券数据为CouponCard组件所需格式
|
||||
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
|
||||
// 判断优惠券状态
|
||||
let status: 0 | 1 | 2 = 0 // 默认未使用
|
||||
if (coupon.isExpire === 1) {
|
||||
status = 2 // 已过期
|
||||
} else if (coupon.status === 1) {
|
||||
status = 1 // 已使用
|
||||
}
|
||||
|
||||
// 根据优惠券类型计算金额显示
|
||||
let amount = 0
|
||||
let type: 1 | 2 | 3 = 1
|
||||
|
||||
if (coupon.type === 10) { // 满减券
|
||||
type = 1
|
||||
amount = parseFloat(coupon.reducePrice || '0')
|
||||
} else if (coupon.type === 20) { // 折扣券
|
||||
type = 2
|
||||
amount = coupon.discount || 0
|
||||
} else if (coupon.type === 30) { // 免费券
|
||||
type = 3
|
||||
amount = 0
|
||||
}
|
||||
|
||||
return {
|
||||
amount,
|
||||
type,
|
||||
status,
|
||||
minAmount: parseFloat(coupon.minPrice || '0'),
|
||||
title: coupon.name || '优惠券',
|
||||
startTime: coupon.startTime,
|
||||
endTime: coupon.endTime,
|
||||
showUseBtn: status === 0, // 只有未使用的券显示使用按钮
|
||||
onUse: () => handleUseCoupon(coupon),
|
||||
theme: getThemeByType(coupon.type)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据优惠券类型获取主题色
|
||||
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
|
||||
switch (type) {
|
||||
case 10: return 'red' // 满减券
|
||||
case 20: return 'orange' // 折扣券
|
||||
case 30: return 'green' // 免费券
|
||||
default: return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优惠券
|
||||
const handleUseCoupon = (coupon: ShopCoupon) => {
|
||||
Taro.showModal({
|
||||
title: '使用优惠券',
|
||||
content: `确定要使用"${coupon.name}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 这里可以跳转到商品页面或购物车页面
|
||||
Taro.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 优惠券点击事件
|
||||
const handleCouponClick = (coupon: CouponCardProps, index: number) => {
|
||||
const originalCoupon = list[index]
|
||||
if (originalCoupon) {
|
||||
// 显示优惠券详情
|
||||
showCouponDetail(originalCoupon)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示优惠券详情
|
||||
const showCouponDetail = (coupon: ShopCoupon) => {
|
||||
// 跳转到优惠券详情页
|
||||
Taro.navigateTo({
|
||||
url: `/user/coupon/detail?id=${coupon.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载优惠券统计数据
|
||||
const loadCouponStats = async () => {
|
||||
try {
|
||||
// 并行获取各状态的优惠券数量
|
||||
const [availableRes, usedRes, expiredRes] = await Promise.all([
|
||||
pageShopCoupon({ page: 1, limit: 1, status: 0, isExpire: 0 }),
|
||||
pageShopCoupon({ page: 1, limit: 1, status: 1 }),
|
||||
pageShopCoupon({ page: 1, limit: 1, isExpire: 1 })
|
||||
])
|
||||
|
||||
setStats({
|
||||
available: availableRes?.count || 0,
|
||||
used: usedRes?.count || 0,
|
||||
expired: expiredRes?.count || 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取优惠券统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片点击事件
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired') => {
|
||||
const tabMap = {
|
||||
available: '0',
|
||||
used: '1',
|
||||
expired: '2'
|
||||
}
|
||||
handleTabChange(tabMap[type])
|
||||
}
|
||||
|
||||
// 筛选条件变更
|
||||
const handleFiltersChange = (newFilters: any) => {
|
||||
setFilters(newFilters)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
// 检查即将过期的优惠券
|
||||
const checkExpiringSoonCoupons = async () => {
|
||||
try {
|
||||
// 获取即将过期的优惠券(3天内过期)
|
||||
const res = await pageShopCoupon({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
status: 0, // 未使用
|
||||
isExpire: 0 // 未过期
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const now = dayjs()
|
||||
const expiringSoon = res.list
|
||||
.map(coupon => {
|
||||
const endTime = dayjs(coupon.endTime)
|
||||
const daysLeft = endTime.diff(now, 'day')
|
||||
|
||||
return {
|
||||
id: coupon.id || 0,
|
||||
name: coupon.name || '',
|
||||
type: coupon.type || 10,
|
||||
amount: coupon.type === 10 ? coupon.reducePrice || '0' :
|
||||
coupon.type === 20 ? coupon.discount?.toString() || '0' : '0',
|
||||
minAmount: coupon.minPrice,
|
||||
endTime: coupon.endTime || '',
|
||||
daysLeft
|
||||
}
|
||||
})
|
||||
.filter(coupon => coupon.daysLeft >= 0 && coupon.daysLeft <= 3)
|
||||
.sort((a, b) => a.daysLeft - b.daysLeft)
|
||||
|
||||
if (expiringSoon.length > 0) {
|
||||
setExpiringSoonCoupons(expiringSoon)
|
||||
// 延迟显示提醒,避免与页面加载冲突
|
||||
setTimeout(() => {
|
||||
setShowExpireNotice(true)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查即将过期优惠券失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用即将过期的优惠券
|
||||
const handleUseExpiringSoonCoupon = (coupon: ExpiringSoon) => {
|
||||
setShowExpireNotice(false)
|
||||
// 跳转到商品页面
|
||||
Taro.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false) // 不刷新,追加数据
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
loadCouponStats().then()
|
||||
// 只在可用优惠券tab时检查即将过期的优惠券
|
||||
if (activeTab === '0') {
|
||||
checkExpiringSoonCoupons().then()
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 搜索栏和领取入口 */}
|
||||
<View className="bg-white px-4 py-3">
|
||||
<View className="flex items-center gap-3">
|
||||
<View className="flex-1">
|
||||
<SearchBar
|
||||
placeholder="搜索优惠券名称"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={handleSearch}
|
||||
leftIcon={<Search />}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Plus />}
|
||||
onClick={() => Taro.navigateTo({url: '/user/coupon/receive'})}
|
||||
>
|
||||
领取
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Filter />}
|
||||
onClick={() => setShowFilter(true)}
|
||||
>
|
||||
筛选
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setShowGuide(true)}
|
||||
>
|
||||
帮助
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 优惠券统计 */}
|
||||
<CouponStats
|
||||
availableCount={stats.available}
|
||||
usedCount={stats.used}
|
||||
expiredCount={stats.expired}
|
||||
onStatsClick={handleStatsClick}
|
||||
/>
|
||||
|
||||
{/* Tab切换 */}
|
||||
<View className="bg-white">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<TabPane title="可用" value="0">
|
||||
</TabPane>
|
||||
<TabPane title="已使用" value="1">
|
||||
</TabPane>
|
||||
<TabPane title="已过期" value="2">
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{total > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50">
|
||||
共找到 {total} 张优惠券
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 优惠券列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View style={{ height: 'calc(100vh - 200px)', overflowY: 'auto' }} id="coupon-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 300px)'}}>
|
||||
<Empty
|
||||
description={
|
||||
activeTab === '0' ? "暂无可用优惠券" :
|
||||
activeTab === '1' ? "暂无已使用优惠券" :
|
||||
"暂无已过期优惠券"
|
||||
}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="coupon-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<CouponList
|
||||
coupons={list.map(transformCouponData)}
|
||||
onCouponClick={handleCouponClick}
|
||||
showEmpty={false}
|
||||
/>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
{/* 底部提示 */}
|
||||
{activeTab === '0' && list.length === 0 && !loading && (
|
||||
<View className="text-center py-8">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<Gift size="48" />
|
||||
</View>
|
||||
<View className="text-gray-500 mb-2">暂无可用优惠券</View>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => Taro.navigateTo({url: '/pages/index/index'})}
|
||||
>
|
||||
去逛逛
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 使用指南弹窗 */}
|
||||
<CouponGuide
|
||||
visible={showGuide}
|
||||
onClose={() => setShowGuide(false)}
|
||||
/>
|
||||
|
||||
{/* 筛选弹窗 */}
|
||||
<CouponFilter
|
||||
visible={showFilter}
|
||||
filters={filters}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onClose={() => setShowFilter(false)}
|
||||
/>
|
||||
|
||||
{/* 到期提醒弹窗 */}
|
||||
<CouponExpireNotice
|
||||
visible={showExpireNotice}
|
||||
expiringSoonCoupons={expiringSoonCoupons}
|
||||
onClose={() => setShowExpireNotice(false)}
|
||||
onUseCoupon={handleUseExpiringSoonCoupon}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default CouponManage;
|
||||
5
src/user/coupon/receive.config.ts
Normal file
5
src/user/coupon/receive.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '领取优惠券',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
248
src/user/coupon/receive.tsx
Normal file
248
src/user/coupon/receive.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Empty, ConfigProvider, SearchBar, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
|
||||
import {Gift, Search} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
|
||||
import {pageShopCoupon} from "@/api/shop/shopCoupon";
|
||||
import CouponList from "@/components/CouponList";
|
||||
import {CouponCardProps} from "@/components/CouponCard";
|
||||
|
||||
const CouponReceive = () => {
|
||||
const [list, setList] = useState<ShopCoupon[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
// 获取可领取的优惠券(启用状态且未过期)
|
||||
const res = await pageShopCoupon({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: searchValue,
|
||||
enabled: '1', // 启用状态
|
||||
isExpire: 0 // 未过期
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
setTotal(res.count || 0)
|
||||
|
||||
setHasMore(res.list.length === 10)
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2)
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠券失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取优惠券失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchValue(value)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await reload(true)
|
||||
}
|
||||
|
||||
// 转换优惠券数据为CouponCard组件所需格式
|
||||
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
|
||||
let amount = 0
|
||||
let type: 1 | 2 | 3 = 1
|
||||
|
||||
if (coupon.type === 10) { // 满减券
|
||||
type = 1
|
||||
amount = parseFloat(coupon.reducePrice || '0')
|
||||
} else if (coupon.type === 20) { // 折扣券
|
||||
type = 2
|
||||
amount = coupon.discount || 0
|
||||
} else if (coupon.type === 30) { // 免费券
|
||||
type = 3
|
||||
amount = 0
|
||||
}
|
||||
|
||||
return {
|
||||
amount,
|
||||
type,
|
||||
status: 0, // 可领取状态
|
||||
minAmount: parseFloat(coupon.minPrice || '0'),
|
||||
title: coupon.name || '优惠券',
|
||||
startTime: coupon.startTime,
|
||||
endTime: coupon.endTime,
|
||||
showReceiveBtn: true, // 显示领取按钮
|
||||
onReceive: () => handleReceiveCoupon(coupon),
|
||||
theme: getThemeByType(coupon.type)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据优惠券类型获取主题色
|
||||
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
|
||||
switch (type) {
|
||||
case 10: return 'red' // 满减券
|
||||
case 20: return 'orange' // 折扣券
|
||||
case 30: return 'green' // 免费券
|
||||
default: return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
// 领取优惠券
|
||||
const handleReceiveCoupon = async (coupon: ShopCoupon) => {
|
||||
try {
|
||||
// 这里应该调用领取优惠券的API
|
||||
// await receiveCoupon(coupon.id)
|
||||
|
||||
Taro.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
reload(true)
|
||||
} catch (error) {
|
||||
console.error('领取优惠券失败:', error)
|
||||
Taro.showToast({
|
||||
title: '领取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 优惠券点击事件
|
||||
const handleCouponClick = (coupon: CouponCardProps, index: number) => {
|
||||
const originalCoupon = list[index]
|
||||
if (originalCoupon) {
|
||||
// 显示优惠券详情
|
||||
showCouponDetail(originalCoupon)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示优惠券详情
|
||||
const showCouponDetail = (coupon: ShopCoupon) => {
|
||||
// 跳转到优惠券详情页
|
||||
Taro.navigateTo({
|
||||
url: `/user/coupon/detail?id=${coupon.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false)
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 搜索栏 */}
|
||||
<View className="bg-white px-4 py-3">
|
||||
<SearchBar
|
||||
placeholder="搜索优惠券名称"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={handleSearch}
|
||||
leftIcon={<Search />}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{total > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50">
|
||||
共找到 {total} 张可领取优惠券
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 优惠券列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="coupon-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 250px)'}}>
|
||||
<Empty
|
||||
description="暂无可领取优惠券"
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
className="mt-4"
|
||||
onClick={() => Taro.navigateTo({url: '/pages/index/index'})}
|
||||
>
|
||||
去逛逛
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="coupon-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<CouponList
|
||||
coupons={list.map(transformCouponData)}
|
||||
onCouponClick={handleCouponClick}
|
||||
showEmpty={false}
|
||||
/>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
{/* 底部提示 */}
|
||||
{list.length === 0 && !loading && (
|
||||
<View className="text-center py-8">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<Gift size="48" />
|
||||
</View>
|
||||
<View className="text-gray-500 mb-2">暂无可领取优惠券</View>
|
||||
<View className="text-gray-400 text-sm">请关注商家活动获取更多优惠券</View>
|
||||
</View>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CouponReceive;
|
||||
4
src/user/gift/add.config.ts
Normal file
4
src/user/gift/add.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '新增收货地址',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
323
src/user/gift/add.tsx
Normal file
323
src/user/gift/add.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, Loading, CellGroup, Input, TextArea, Form, Switch, InputNumber, Radio, Image} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Upload as UploadIcon} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopArticle} from "@/api/shop/shopArticle/model";
|
||||
import {getShopArticle, addShopArticle, updateShopArticle} from "@/api/shop/shopArticle";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
|
||||
const AddShopArticle = () => {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [formData, setFormData] = useState<ShopArticle>({
|
||||
type: 0, // 默认常规文章
|
||||
status: 0, // 默认已发布
|
||||
permission: 0, // 默认所有人可见
|
||||
recommend: 0, // 默认不推荐
|
||||
showType: 10, // 默认小图展示
|
||||
virtualViews: 0, // 默认虚拟阅读量
|
||||
actualViews: 0, // 默认实际阅读量
|
||||
sortNumber: 0 // 默认排序
|
||||
})
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 判断是编辑还是新增模式
|
||||
const isEditMode = !!params.id
|
||||
const articleId = params.id ? Number(params.id) : undefined
|
||||
|
||||
// 文章类型选项
|
||||
const typeOptions = [
|
||||
{ text: '常规文章', value: 0 },
|
||||
{ text: '视频文章', value: 1 }
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ text: '已发布', value: 0 },
|
||||
{ text: '待审核', value: 1 },
|
||||
{ text: '已驳回', value: 2 },
|
||||
{ text: '违规内容', value: 3 }
|
||||
]
|
||||
|
||||
// 可见性选项
|
||||
const permissionOptions = [
|
||||
{ text: '所有人可见', value: 0 },
|
||||
{ text: '登录可见', value: 1 },
|
||||
{ text: '密码可见', value: 2 }
|
||||
]
|
||||
|
||||
// 显示方式选项
|
||||
const showTypeOptions = [
|
||||
{ text: '小图展示', value: 10 },
|
||||
{ text: '大图展示', value: 20 }
|
||||
]
|
||||
|
||||
const reload = async () => {
|
||||
// 如果是编辑模式,加载文章数据
|
||||
if (isEditMode && articleId) {
|
||||
try {
|
||||
const article = await getShopArticle(articleId)
|
||||
setFormData(article)
|
||||
// 更新表单值
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue(article)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文章失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载文章失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传处理
|
||||
const handleImageUpload = async () => {
|
||||
try {
|
||||
const res = await Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
});
|
||||
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
// 这里应该调用上传接口,暂时使用本地路径
|
||||
const imagePath = res.tempFilePaths[0];
|
||||
setFormData({
|
||||
...formData,
|
||||
image: imagePath
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '图片选择成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '图片选择失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...formData,
|
||||
...values,
|
||||
};
|
||||
|
||||
// 如果是编辑模式,添加id
|
||||
if (isEditMode && articleId) {
|
||||
submitData.articleId = articleId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopArticle(submitData);
|
||||
} else {
|
||||
await addShopArticle(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}失败`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 动态设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: isEditMode ? '编辑文章' : '新增文章'
|
||||
});
|
||||
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [isEditMode]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={formData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<CellGroup title="基本信息">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="文章标题"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入文章标题' }]}
|
||||
initialValue={formData.title}
|
||||
>
|
||||
<Input placeholder="请输入文章标题" maxLength={100}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="overview" label="文章概述" initialValue={formData.overview}>
|
||||
<TextArea placeholder="请输入文章概述,用于列表展示" maxLength={200} rows={3}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="detail"
|
||||
label="文章内容"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入文章内容' }]}
|
||||
initialValue={formData.detail}
|
||||
>
|
||||
<TextArea placeholder="请输入文章内容" maxLength={10000} rows={8}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="author" label="作者" initialValue={formData.author}>
|
||||
<Input placeholder="请输入作者名称" maxLength={50}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="source" label="来源" initialValue={formData.source}>
|
||||
<Input placeholder="请输入文章来源" maxLength={100}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 文章设置 */}
|
||||
<CellGroup title="文章设置">
|
||||
<Form.Item name="type" label="文章类型" initialValue={formData.type}>
|
||||
<Radio.Group direction="horizontal" value={formData.type}>
|
||||
{typeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="status" label="发布状态" initialValue={formData.status}>
|
||||
<Radio.Group direction="horizontal" value={formData.status}>
|
||||
{statusOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="permission" label="可见性" initialValue={formData.permission}>
|
||||
<Radio.Group direction="horizontal" value={formData.permission}>
|
||||
{permissionOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="showType" label="显示方式" initialValue={formData.showType}>
|
||||
<Radio.Group direction="horizontal" value={formData.showType}>
|
||||
{showTypeOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 高级设置 */}
|
||||
<CellGroup title="高级设置">
|
||||
<Form.Item name="recommend" label="推荐文章" initialValue={formData.recommend}>
|
||||
<Switch
|
||||
checked={formData.recommend === 1}
|
||||
onChange={(checked) =>
|
||||
setFormData({...formData, recommend: checked ? 1 : 0})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="price" label="付费金额" initialValue={formData.price}>
|
||||
<Input placeholder="0.00" type="number"/>
|
||||
<View className="text-xs text-gray-500 mt-1">单位:元</View>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="virtualViews" label="虚拟阅读量" initialValue={formData.virtualViews}>
|
||||
<InputNumber min={0} defaultValue={formData.virtualViews || 0}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="actualViews" label="实际阅读量" initialValue={formData.actualViews}>
|
||||
<InputNumber min={0} defaultValue={formData.actualViews || 0}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="sortNumber" label="排序" initialValue={formData.sortNumber}>
|
||||
<InputNumber min={0} defaultValue={formData.sortNumber || 0}/>
|
||||
<View className="text-xs text-gray-500 mt-1">数字越小越靠前</View>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="tags" label="标签" initialValue={formData.tags}>
|
||||
<Input placeholder="请输入标签,多个标签用逗号分隔" maxLength={200}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="topic" label="话题" initialValue={formData.topic}>
|
||||
<Input placeholder="请输入话题" maxLength={100}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 图片上传 */}
|
||||
<CellGroup title="文章图片">
|
||||
<Form.Item name="image" label="封面图片" initialValue={formData.image}>
|
||||
<View className="flex items-center gap-3">
|
||||
{formData.image && (
|
||||
<Image
|
||||
src={formData.image}
|
||||
width="80"
|
||||
height="80"
|
||||
radius="8"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
fill="outline"
|
||||
icon={<UploadIcon />}
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
{formData.image ? '更换图片' : '上传图片'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<FixedButton text={isEditMode ? '更新文章' : '发布文章'} onClick={submitSucceed} icon={<Edit />} />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddShopArticle;
|
||||
6
src/user/gift/detail.config.ts
Normal file
6
src/user/gift/detail.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '礼品卡详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
345
src/user/gift/detail.tsx
Normal file
345
src/user/gift/detail.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import {useState, useEffect} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, ConfigProvider, Tag, Divider, Image} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft, Gift, Clock, Location, Phone, Share, Copy} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ShopGift} from "@/api/shop/shopGift/model";
|
||||
import {getShopGift} from "@/api/shop/shopGift";
|
||||
import GiftCardShare from "@/components/GiftCardShare";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const GiftCardDetail = () => {
|
||||
const router = useRouter()
|
||||
const [gift, setGift] = useState<ShopGift | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showShare, setShowShare] = useState(false)
|
||||
const giftId = router.params.id
|
||||
|
||||
useEffect(() => {
|
||||
if (giftId) {
|
||||
loadGiftDetail()
|
||||
}
|
||||
}, [giftId])
|
||||
|
||||
const loadGiftDetail = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await getShopGift(Number(giftId))
|
||||
setGift(data)
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡详情失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡详情失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡类型文本
|
||||
const getGiftTypeText = (type?: number) => {
|
||||
switch (type) {
|
||||
case 10: return '实物礼品卡'
|
||||
case 20: return '虚拟礼品卡'
|
||||
case 30: return '服务礼品卡'
|
||||
default: return '礼品卡'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡面值显示
|
||||
const getGiftValueDisplay = () => {
|
||||
if (!gift || !gift.faceValue) return ''
|
||||
return `¥${gift.faceValue}`
|
||||
}
|
||||
|
||||
// 获取使用条件文本
|
||||
const getUsageText = () => {
|
||||
if (!gift) return ''
|
||||
|
||||
if (gift.instructions) {
|
||||
return gift.instructions
|
||||
}
|
||||
|
||||
switch (gift.type) {
|
||||
case 10: return '请到指定门店使用此礼品卡'
|
||||
case 20: return '可在线上平台直接使用'
|
||||
case 30: return '请联系客服预约服务时间'
|
||||
default: return '请按照使用说明进行操作'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取有效期文本
|
||||
const getValidityText = () => {
|
||||
if (!gift) return ''
|
||||
|
||||
if (gift.validDays) {
|
||||
return `有效期${gift.validDays}天`
|
||||
} else if (gift.expireTime) {
|
||||
return `有效期至 ${dayjs(gift.expireTime).format('YYYY年MM月DD日')}`
|
||||
} else {
|
||||
return '长期有效'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取礼品卡状态
|
||||
const getGiftStatus = () => {
|
||||
if (!gift) return { status: 0, text: '未知', color: 'default' }
|
||||
|
||||
switch (gift.useStatus) {
|
||||
case 0:
|
||||
return { status: 0, text: '可使用', color: 'success' }
|
||||
case 1:
|
||||
return { status: 1, text: '已使用', color: 'warning' }
|
||||
case 2:
|
||||
return { status: 2, text: '已过期', color: 'danger' }
|
||||
default:
|
||||
return { status: 0, text: '未知', color: 'default' }
|
||||
}
|
||||
}
|
||||
|
||||
// 使用礼品卡
|
||||
const handleUseGift = () => {
|
||||
if (!gift) return
|
||||
|
||||
Taro.showModal({
|
||||
title: '使用礼品卡',
|
||||
content: `确定要使用"${gift.name}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到礼品卡使用页面
|
||||
Taro.navigateTo({
|
||||
url: `/user/gift/use?id=${gift.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制兑换码
|
||||
const handleCopyCode = () => {
|
||||
if (!gift?.code) return
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: gift.code,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '兑换码已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex justify-center items-center h-screen">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (!gift) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex flex-col justify-center items-center h-screen">
|
||||
<Text className="text-gray-500 mb-4">礼品卡不存在</Text>
|
||||
<Button onClick={handleBack}>返回</Button>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = getGiftStatus()
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 自定义导航栏 */}
|
||||
<View className="flex items-center justify-between p-4 bg-white border-b border-gray-100">
|
||||
<View className="flex items-center" onClick={handleBack}>
|
||||
<ArrowLeft size="20" />
|
||||
<Text className="ml-2 text-lg">礼品卡详情</Text>
|
||||
</View>
|
||||
<View className="flex items-center gap-3">
|
||||
<View onClick={() => setShowShare(true)}>
|
||||
<Share size="20" className="text-gray-600" />
|
||||
</View>
|
||||
<Tag type={statusInfo.color as any}>{statusInfo.text}</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡卡片 */}
|
||||
<View className="m-4 p-6 rounded-2xl text-white" style={{backgroundColor: '#fbbf24'}}>
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<View className="flex-1">
|
||||
<Text className="text-4xl font-bold">{getGiftValueDisplay()}</Text>
|
||||
<Text className="text-lg opacity-90 mt-1">{getGiftTypeText(gift.type)}</Text>
|
||||
</View>
|
||||
{gift.goodsImage ? (
|
||||
<Image
|
||||
src={gift.goodsImage}
|
||||
className="w-16 h-16 rounded-lg"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
) : (
|
||||
<Gift size="40" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text className="text-xl font-semibold mb-2">{gift.name}</Text>
|
||||
<Text className="text-base opacity-90">{gift.description || getUsageText()}</Text>
|
||||
|
||||
{/* 兑换码 */}
|
||||
{gift.code && (
|
||||
<View className="mt-4 p-3 bg-white bg-opacity-20 rounded-lg">
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-sm opacity-80">兑换码</Text>
|
||||
<Text className="text-lg font-mono font-bold">{gift.code}</Text>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Copy />}
|
||||
onClick={handleCopyCode}
|
||||
className="border-white text-white"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<View className="bg-white mx-4 rounded-xl p-4">
|
||||
<Text className="text-lg font-semibold mb-4">使用说明</Text>
|
||||
|
||||
<View>
|
||||
<View className="flex items-center mb-3">
|
||||
<Clock size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">有效期</Text>
|
||||
<Text className="text-gray-900">{getValidityText()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View className="flex items-center mb-3">
|
||||
<Gift size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">礼品卡类型</Text>
|
||||
<Text className="text-gray-900">{getGiftTypeText(gift.type)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{gift.useLocation && (
|
||||
<>
|
||||
<Divider />
|
||||
<View className="flex items-center">
|
||||
<Location size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">使用地址</Text>
|
||||
<Text className="text-gray-900">{gift.useLocation}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gift.contactInfo && (
|
||||
<>
|
||||
<Divider />
|
||||
<View className="flex items-center">
|
||||
<Phone size="16" className="text-gray-400 mr-3" />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm">客服联系</Text>
|
||||
<Text className="text-gray-900">{gift.contactInfo}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gift.instructions && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm mb-2">使用说明</Text>
|
||||
<Text className="text-gray-900 leading-relaxed">{gift.instructions}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{gift.useTime && (
|
||||
<>
|
||||
<Divider />
|
||||
<View>
|
||||
<Text className="text-gray-600 text-sm mb-2">使用记录</Text>
|
||||
<Text className="text-gray-900">使用时间:{dayjs(gift.useTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
{statusInfo.status === 0 && (
|
||||
<View className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t border-gray-100">
|
||||
<View className="flex gap-3">
|
||||
{gift.code && (
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
icon={<Copy />}
|
||||
onClick={handleCopyCode}
|
||||
>
|
||||
复制兑换码
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={handleUseGift}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 分享弹窗 */}
|
||||
{gift && (
|
||||
<GiftCardShare
|
||||
visible={showShare}
|
||||
giftCard={{
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '',
|
||||
type: gift.type || 10,
|
||||
faceValue: gift.faceValue || '0',
|
||||
code: gift.code,
|
||||
description: gift.description
|
||||
}}
|
||||
onClose={() => setShowShare(false)}
|
||||
/>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiftCardDetail;
|
||||
5
src/user/gift/index.config.ts
Normal file
5
src/user/gift/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的礼品卡',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
403
src/user/gift/index.tsx
Normal file
403
src/user/gift/index.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Empty, ConfigProvider, InfiniteLoading, Loading, PullToRefresh, Tabs, TabPane} from '@nutui/nutui-react-taro'
|
||||
import {Gift, Plus, Board, QrCode} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopGift} from "@/api/shop/shopGift/model";
|
||||
import {getUserGifts} from "@/api/shop/shopGift";
|
||||
import GiftCardList from "@/components/GiftCardList";
|
||||
import GiftCardStats from "@/components/GiftCardStats";
|
||||
import GiftCardGuide from "@/components/GiftCardGuide";
|
||||
import {GiftCardProps} from "@/components/GiftCard";
|
||||
|
||||
const GiftCardManage = () => {
|
||||
const [list, setList] = useState<ShopGift[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
// const [searchValue, setSearchValue] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
// const [total, setTotal] = useState(0)
|
||||
const [activeTab, setActiveTab] = useState('0') // 0-可用 1-已使用 2-已过期
|
||||
const [stats, setStats] = useState({
|
||||
available: 0,
|
||||
used: 0,
|
||||
expired: 0,
|
||||
totalValue: 0
|
||||
})
|
||||
const [showGuide, setShowGuide] = useState(false)
|
||||
// const [showRedeemModal, setShowRedeemModal] = useState(false)
|
||||
// const [filters, setFilters] = useState({
|
||||
// type: [] as number[],
|
||||
// sortBy: 'createTime' as 'createTime' | 'expireTime' | 'faceValue' | 'useTime',
|
||||
// sortOrder: 'desc' as 'asc' | 'desc'
|
||||
// })
|
||||
|
||||
// 获取礼品卡状态过滤条件
|
||||
const getStatusFilter = () => {
|
||||
switch (activeTab) {
|
||||
case '0': // 可用
|
||||
return { useStatus: 0 }
|
||||
case '1': // 已使用
|
||||
return { useStatus: 1 }
|
||||
case '2': // 已过期
|
||||
return { useStatus: 2 }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
const statusFilter = getStatusFilter()
|
||||
const res = await getUserGifts({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
// keywords: searchValue,
|
||||
...statusFilter,
|
||||
// 应用筛选条件
|
||||
// ...(filters.type.length > 0 && { type: filters.type[0] }),
|
||||
// sortBy: filters.sortBy,
|
||||
// sortOrder: filters.sortOrder
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
// setTotal(res.count || 0)
|
||||
|
||||
// 判断是否还有更多数据
|
||||
setHasMore(res.list.length === 10) // 如果返回的数据等于limit,说明可能还有更多
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2) // 刷新后下一页是第2页
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
// setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
// const handleSearch = (value: string) => {
|
||||
// setSearchValue(value)
|
||||
// reload(true)
|
||||
// }
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await reload(true)
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value)
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
// 延迟执行reload,确保状态更新完成
|
||||
setTimeout(() => {
|
||||
reload(true)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 转换礼品卡数据为GiftCard组件所需格式
|
||||
const transformGiftData = (gift: ShopGift): GiftCardProps => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡',
|
||||
description: gift.description,
|
||||
code: gift.code,
|
||||
goodsImage: gift.goodsImage,
|
||||
faceValue: gift.faceValue,
|
||||
type: gift.type,
|
||||
useStatus: gift.useStatus,
|
||||
expireTime: gift.expireTime,
|
||||
useTime: gift.useTime,
|
||||
useLocation: gift.useLocation,
|
||||
contactInfo: gift.contactInfo,
|
||||
showCode: gift.useStatus === 0, // 只有可用状态显示兑换码
|
||||
showUseBtn: gift.useStatus === 0, // 只有可用状态显示使用按钮
|
||||
showDetailBtn: true,
|
||||
theme: getThemeByType(gift.type),
|
||||
onUse: () => handleUseGift(gift),
|
||||
onDetail: () => handleGiftDetail(gift)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据礼品卡类型获取主题色
|
||||
const getThemeByType = (type?: number): 'gold' | 'silver' | 'bronze' | 'blue' | 'green' | 'purple' => {
|
||||
switch (type) {
|
||||
case 10: return 'gold' // 实物礼品卡
|
||||
case 20: return 'blue' // 虚拟礼品卡
|
||||
case 30: return 'green' // 服务礼品卡
|
||||
default: return 'silver'
|
||||
}
|
||||
}
|
||||
|
||||
// 使用礼品卡
|
||||
const handleUseGift = (gift: ShopGift) => {
|
||||
Taro.showModal({
|
||||
title: '使用礼品卡',
|
||||
content: `确定要使用"${gift.name}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到礼品卡使用页面
|
||||
Taro.navigateTo({
|
||||
url: `/user/gift/use?id=${gift.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 礼品卡点击事件
|
||||
const handleGiftClick = (gift: GiftCardProps, index: number) => {
|
||||
console.log(gift.code)
|
||||
const originalGift = list[index]
|
||||
if (originalGift) {
|
||||
// 显示礼品卡详情
|
||||
handleGiftDetail(originalGift)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示礼品卡详情
|
||||
const handleGiftDetail = (gift: ShopGift) => {
|
||||
// 跳转到礼品卡详情页
|
||||
Taro.navigateTo({
|
||||
url: `/user/gift/detail?id=${gift.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载礼品卡统计数据
|
||||
const loadGiftStats = async () => {
|
||||
try {
|
||||
// 并行获取各状态的礼品卡数量
|
||||
const [availableRes, usedRes, expiredRes] = await Promise.all([
|
||||
getUserGifts({ page: 1, limit: 1, useStatus: 0 }),
|
||||
getUserGifts({ page: 1, limit: 1, useStatus: 1 }),
|
||||
getUserGifts({ page: 1, limit: 1, useStatus: 2 })
|
||||
])
|
||||
|
||||
// 计算总价值(仅可用礼品卡)
|
||||
const availableGifts = await getUserGifts({ page: 1, limit: 100, useStatus: 0 })
|
||||
const totalValue = availableGifts?.list?.reduce((sum, gift) => {
|
||||
return sum + parseFloat(gift.faceValue || '0')
|
||||
}, 0) || 0
|
||||
|
||||
setStats({
|
||||
available: availableRes?.count || 0,
|
||||
used: usedRes?.count || 0,
|
||||
expired: expiredRes?.count || 0,
|
||||
totalValue
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片点击事件
|
||||
const handleStatsClick = (type: 'available' | 'used' | 'expired' | 'total') => {
|
||||
const tabMap = {
|
||||
available: '0',
|
||||
used: '1',
|
||||
expired: '2',
|
||||
total: '0' // 总价值点击跳转到可用礼品卡
|
||||
}
|
||||
if (tabMap[type]) {
|
||||
handleTabChange(tabMap[type])
|
||||
}
|
||||
}
|
||||
|
||||
// 兑换礼品卡
|
||||
const handleRedeemGift = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/user/gift/redeem'
|
||||
})
|
||||
}
|
||||
|
||||
// 扫码兑换礼品卡
|
||||
const handleScanRedeem = () => {
|
||||
Taro.scanCode({
|
||||
success: (res) => {
|
||||
// 处理扫码结果
|
||||
const code = res.result
|
||||
if (code) {
|
||||
Taro.navigateTo({
|
||||
url: `/user/gift/redeem?code=${encodeURIComponent(code)}`
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '扫码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false) // 不刷新,追加数据
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
loadGiftStats().then()
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 搜索栏和功能入口 */}
|
||||
<View className="bg-white px-4 py-3">
|
||||
<View className="flex items-center justify-between gap-3">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<Plus />}
|
||||
onClick={handleRedeemGift}
|
||||
>
|
||||
兑换
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<QrCode />}
|
||||
onClick={handleScanRedeem}
|
||||
>
|
||||
扫码
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Board />}
|
||||
onClick={() => setShowGuide(true)}
|
||||
>
|
||||
帮助
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡统计 */}
|
||||
<GiftCardStats
|
||||
availableCount={stats.available}
|
||||
usedCount={stats.used}
|
||||
expiredCount={stats.expired}
|
||||
totalValue={stats.totalValue}
|
||||
onStatsClick={handleStatsClick}
|
||||
/>
|
||||
|
||||
{/* Tab切换 */}
|
||||
<View className="bg-white">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<TabPane title="可用" value="0">
|
||||
</TabPane>
|
||||
<TabPane title="已使用" value="1">
|
||||
</TabPane>
|
||||
<TabPane title="已过期" value="2">
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 礼品卡列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View style={{ height: '600px', overflowY: 'auto' }} id="gift-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{height: '500px'}}>
|
||||
<Empty
|
||||
description={
|
||||
activeTab === '0' ? "暂无可用礼品卡" :
|
||||
activeTab === '1' ? "暂无已使用礼品卡" :
|
||||
"暂无已过期礼品卡"
|
||||
}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="gift-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<GiftCardList
|
||||
gifts={list.map(transformGiftData)}
|
||||
onGiftClick={handleGiftClick}
|
||||
showEmpty={false}
|
||||
/>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
{/* 底部提示 */}
|
||||
{activeTab === '0' && list.length === 0 && !loading && (
|
||||
<View className="text-center py-8">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<Gift size="48" />
|
||||
</View>
|
||||
<View className="text-gray-500 mb-2">暂无可用礼品卡</View>
|
||||
<View className="flex gap-2 justify-center">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleRedeemGift}
|
||||
>
|
||||
兑换礼品卡
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={handleScanRedeem}
|
||||
>
|
||||
扫码兑换
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 使用指南弹窗 */}
|
||||
<GiftCardGuide
|
||||
visible={showGuide}
|
||||
onClose={() => setShowGuide(false)}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default GiftCardManage;
|
||||
5
src/user/gift/receive.config.ts
Normal file
5
src/user/gift/receive.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '领取优惠券',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
248
src/user/gift/receive.tsx
Normal file
248
src/user/gift/receive.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Empty, ConfigProvider, SearchBar, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
|
||||
import {Gift, Search} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
|
||||
import {pageShopCoupon} from "@/api/shop/shopCoupon";
|
||||
import CouponList from "@/components/CouponList";
|
||||
import {CouponCardProps} from "@/components/CouponCard";
|
||||
|
||||
const CouponReceive = () => {
|
||||
const [list, setList] = useState<ShopCoupon[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
const reload = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setPage(1)
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const currentPage = isRefresh ? 1 : page
|
||||
// 获取可领取的优惠券(启用状态且未过期)
|
||||
const res = await pageShopCoupon({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
keywords: searchValue,
|
||||
enabled: '1', // 启用状态
|
||||
isExpire: 0 // 未过期
|
||||
})
|
||||
|
||||
if (res && res.list) {
|
||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
||||
setList(newList)
|
||||
setTotal(res.count || 0)
|
||||
|
||||
setHasMore(res.list.length === 10)
|
||||
|
||||
if (!isRefresh) {
|
||||
setPage(currentPage + 1)
|
||||
} else {
|
||||
setPage(2)
|
||||
}
|
||||
} else {
|
||||
setHasMore(false)
|
||||
setTotal(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取优惠券失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取优惠券失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchValue(value)
|
||||
reload(true)
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await reload(true)
|
||||
}
|
||||
|
||||
// 转换优惠券数据为CouponCard组件所需格式
|
||||
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
|
||||
let amount = 0
|
||||
let type: 1 | 2 | 3 = 1
|
||||
|
||||
if (coupon.type === 10) { // 满减券
|
||||
type = 1
|
||||
amount = parseFloat(coupon.reducePrice || '0')
|
||||
} else if (coupon.type === 20) { // 折扣券
|
||||
type = 2
|
||||
amount = coupon.discount || 0
|
||||
} else if (coupon.type === 30) { // 免费券
|
||||
type = 3
|
||||
amount = 0
|
||||
}
|
||||
|
||||
return {
|
||||
amount,
|
||||
type,
|
||||
status: 0, // 可领取状态
|
||||
minAmount: parseFloat(coupon.minPrice || '0'),
|
||||
title: coupon.name || '优惠券',
|
||||
startTime: coupon.startTime,
|
||||
endTime: coupon.endTime,
|
||||
showReceiveBtn: true, // 显示领取按钮
|
||||
onReceive: () => handleReceiveCoupon(coupon),
|
||||
theme: getThemeByType(coupon.type)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据优惠券类型获取主题色
|
||||
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
|
||||
switch (type) {
|
||||
case 10: return 'red' // 满减券
|
||||
case 20: return 'orange' // 折扣券
|
||||
case 30: return 'green' // 免费券
|
||||
default: return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
// 领取优惠券
|
||||
const handleReceiveCoupon = async (coupon: ShopCoupon) => {
|
||||
try {
|
||||
// 这里应该调用领取优惠券的API
|
||||
// await receiveCoupon(coupon.id)
|
||||
|
||||
Taro.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
reload(true)
|
||||
} catch (error) {
|
||||
console.error('领取优惠券失败:', error)
|
||||
Taro.showToast({
|
||||
title: '领取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 优惠券点击事件
|
||||
const handleCouponClick = (coupon: CouponCardProps, index: number) => {
|
||||
const originalCoupon = list[index]
|
||||
if (originalCoupon) {
|
||||
// 显示优惠券详情
|
||||
showCouponDetail(originalCoupon)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示优惠券详情
|
||||
const showCouponDetail = (coupon: ShopCoupon) => {
|
||||
// 跳转到优惠券详情页
|
||||
Taro.navigateTo({
|
||||
url: `/user/coupon/detail?id=${coupon.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (!loading && hasMore) {
|
||||
await reload(false)
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload(true).then()
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 搜索栏 */}
|
||||
<View className="bg-white px-4 py-3">
|
||||
<SearchBar
|
||||
placeholder="搜索优惠券名称"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={handleSearch}
|
||||
leftIcon={<Search />}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{total > 0 && (
|
||||
<View className="px-4 py-2 text-sm text-gray-500 bg-gray-50">
|
||||
共找到 {total} 张可领取优惠券
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 优惠券列表 */}
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
headHeight={60}
|
||||
>
|
||||
<View style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="coupon-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 250px)'}}>
|
||||
<Empty
|
||||
description="暂无可领取优惠券"
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
className="mt-4"
|
||||
onClick={() => Taro.navigateTo({url: '/pages/index/index'})}
|
||||
>
|
||||
去逛逛
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="coupon-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<CouponList
|
||||
coupons={list.map(transformCouponData)}
|
||||
onCouponClick={handleCouponClick}
|
||||
showEmpty={false}
|
||||
/>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
{/* 底部提示 */}
|
||||
{list.length === 0 && !loading && (
|
||||
<View className="text-center py-8">
|
||||
<View className="text-gray-400 mb-4">
|
||||
<Gift size="48" />
|
||||
</View>
|
||||
<View className="text-gray-500 mb-2">暂无可领取优惠券</View>
|
||||
<View className="text-gray-400 text-sm">请关注商家活动获取更多优惠券</View>
|
||||
</View>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CouponReceive;
|
||||
6
src/user/gift/redeem.config.ts
Normal file
6
src/user/gift/redeem.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '兑换礼品卡',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
266
src/user/gift/redeem.tsx
Normal file
266
src/user/gift/redeem.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import {useState, useEffect} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, ConfigProvider, Input, Divider} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft, QrCode, Gift, Voucher} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ShopGift} from "@/api/shop/shopGift/model";
|
||||
import {validateGiftCode, redeemGift} from "@/api/shop/shopGift";
|
||||
import GiftCard from "@/components/GiftCard";
|
||||
|
||||
const GiftCardRedeem = () => {
|
||||
const router = useRouter()
|
||||
const [code, setCode] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [validGift, setValidGift] = useState<ShopGift | null>(null)
|
||||
const [redeemSuccess, setRedeemSuccess] = useState(false)
|
||||
|
||||
// 从路由参数获取扫码结果
|
||||
useEffect(() => {
|
||||
if (router.params.code) {
|
||||
const scannedCode = decodeURIComponent(router.params.code)
|
||||
setCode(scannedCode)
|
||||
handleValidateCode(scannedCode)
|
||||
}
|
||||
}, [router.params.code])
|
||||
|
||||
// 验证兑换码
|
||||
const handleValidateCode = async (inputCode?: string) => {
|
||||
const codeToValidate = inputCode || code
|
||||
if (!codeToValidate.trim()) {
|
||||
Taro.showToast({
|
||||
title: '请输入兑换码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setValidating(true)
|
||||
try {
|
||||
const gift = await validateGiftCode(codeToValidate.trim())
|
||||
setValidGift(gift)
|
||||
Taro.showToast({
|
||||
title: '验证成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('验证兑换码失败:', error)
|
||||
setValidGift(null)
|
||||
Taro.showToast({
|
||||
title: '兑换码无效或已使用',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setValidating(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 兑换礼品卡
|
||||
const handleRedeem = async () => {
|
||||
if (!validGift || !code.trim()) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await redeemGift({
|
||||
code: code.trim()
|
||||
})
|
||||
|
||||
setRedeemSuccess(true)
|
||||
Taro.showToast({
|
||||
title: '兑换成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('兑换礼品卡失败:', error)
|
||||
Taro.showToast({
|
||||
title: '兑换失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 扫码兑换
|
||||
const handleScanCode = () => {
|
||||
Taro.scanCode({
|
||||
success: (res) => {
|
||||
const scannedCode = res.result
|
||||
if (scannedCode) {
|
||||
setCode(scannedCode)
|
||||
handleValidateCode(scannedCode)
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '扫码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
// 查看我的礼品卡
|
||||
const handleViewMyGifts = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/user/gift/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 转换礼品卡数据
|
||||
const transformGiftData = (gift: ShopGift) => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡',
|
||||
description: gift.description,
|
||||
code: gift.code,
|
||||
goodsImage: gift.goodsImage,
|
||||
faceValue: gift.faceValue,
|
||||
type: gift.type,
|
||||
useStatus: gift.useStatus,
|
||||
expireTime: gift.expireTime,
|
||||
useTime: gift.useTime,
|
||||
useLocation: gift.useLocation,
|
||||
contactInfo: gift.contactInfo,
|
||||
showCode: false, // 兑换页面不显示兑换码
|
||||
showUseBtn: false, // 兑换页面不显示使用按钮
|
||||
showDetailBtn: false, // 兑换页面不显示详情按钮
|
||||
theme: 'gold' as const
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 自定义导航栏 */}
|
||||
<View className="flex items-center justify-between p-4 bg-white border-b border-gray-100">
|
||||
<View className="flex items-center" onClick={handleBack}>
|
||||
<ArrowLeft size="20" />
|
||||
<Text className="ml-2 text-lg">兑换礼品卡</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!redeemSuccess ? (
|
||||
<>
|
||||
{/* 兑换说明 */}
|
||||
<View className="bg-blue-50 mx-4 mt-4 p-4 rounded-xl border border-blue-200">
|
||||
<View className="flex items-center mb-2">
|
||||
<Gift size="20" className="text-blue-600 mr-2" />
|
||||
<Text className="font-semibold text-blue-800">兑换说明</Text>
|
||||
</View>
|
||||
<Text className="text-blue-700 text-sm leading-relaxed">
|
||||
请输入或扫描礼品卡兑换码,验证成功后即可兑换到您的账户中。每个兑换码只能使用一次。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 兑换码输入 */}
|
||||
<View className="bg-white mx-4 mt-4 p-4 rounded-xl">
|
||||
<Text className="font-semibold mb-3 text-gray-800">输入兑换码</Text>
|
||||
|
||||
<View className="mb-4">
|
||||
<Input
|
||||
placeholder="请输入礼品卡兑换码"
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
clearable
|
||||
className="border border-gray-200 rounded-lg"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex gap-3">
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
icon={<QrCode />}
|
||||
onClick={handleScanCode}
|
||||
>
|
||||
扫码输入
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="flex-1"
|
||||
loading={validating}
|
||||
onClick={() => handleValidateCode()}
|
||||
>
|
||||
验证兑换码
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 验证结果 */}
|
||||
{validGift && (
|
||||
<>
|
||||
<Divider className="my-4">验证成功</Divider>
|
||||
|
||||
<View className="mx-4">
|
||||
<GiftCard {...transformGiftData(validGift)} />
|
||||
</View>
|
||||
|
||||
<View className="mx-4 mt-4">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={loading}
|
||||
onClick={handleRedeem}
|
||||
>
|
||||
确认兑换
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* 兑换成功页面 */
|
||||
<View className="flex flex-col items-center justify-center px-4" style={{height: '600px'}}>
|
||||
<View className="text-center">
|
||||
<View className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Voucher size="40" className="text-green-600" />
|
||||
</View>
|
||||
|
||||
<Text className="text-2xl font-bold text-gray-900 mb-2">兑换成功!</Text>
|
||||
<Text className="text-gray-600 mb-6">礼品卡已添加到您的账户中</Text>
|
||||
|
||||
{validGift && (
|
||||
<View className="mb-6">
|
||||
<GiftCard {...transformGiftData(validGift)} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex flex-col gap-3 w-full max-w-xs">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={handleViewMyGifts}
|
||||
>
|
||||
查看我的礼品卡
|
||||
</Button>
|
||||
<Button
|
||||
fill="outline"
|
||||
size="large"
|
||||
block
|
||||
onClick={() => {
|
||||
setCode('')
|
||||
setValidGift(null)
|
||||
setRedeemSuccess(false)
|
||||
}}
|
||||
>
|
||||
继续兑换
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiftCardRedeem;
|
||||
6
src/user/gift/use.config.ts
Normal file
6
src/user/gift/use.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '使用礼品卡',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
291
src/user/gift/use.tsx
Normal file
291
src/user/gift/use.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import {useState, useEffect} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Button, ConfigProvider, Input, TextArea} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft, Location} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ShopGift} from "@/api/shop/shopGift/model";
|
||||
import {getShopGift, useGift} from "@/api/shop/shopGift";
|
||||
import GiftCard from "@/components/GiftCard";
|
||||
|
||||
const GiftCardUse = () => {
|
||||
const router = useRouter()
|
||||
const [gift, setGift] = useState<ShopGift | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [useLocation, setUseLocation] = useState('')
|
||||
const [useNote, setUseNote] = useState('')
|
||||
const [useSuccess, setUseSuccess] = useState(false)
|
||||
const giftId = router.params.id
|
||||
|
||||
useEffect(() => {
|
||||
if (giftId) {
|
||||
loadGiftDetail()
|
||||
}
|
||||
}, [giftId])
|
||||
|
||||
// 加载礼品卡详情
|
||||
const loadGiftDetail = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await getShopGift(Number(giftId))
|
||||
setGift(data)
|
||||
|
||||
// 如果礼品卡有预设使用地址,自动填入
|
||||
if (data.useLocation) {
|
||||
setUseLocation(data.useLocation)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取礼品卡详情失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取礼品卡详情失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用礼品卡
|
||||
const handleUseGift = async () => {
|
||||
if (!gift) return
|
||||
|
||||
// 根据礼品卡类型进行不同的验证
|
||||
if (gift.type === 10 && !useLocation.trim()) { // 实物礼品卡需要地址
|
||||
Taro.showToast({
|
||||
title: '请填写使用地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await useGift({
|
||||
giftId: gift.id!,
|
||||
useLocation: useLocation.trim(),
|
||||
useNote: useNote.trim()
|
||||
})
|
||||
|
||||
setUseSuccess(true)
|
||||
Taro.showToast({
|
||||
title: '使用成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('使用礼品卡失败:', error)
|
||||
Taro.showToast({
|
||||
title: '使用失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前位置
|
||||
const handleGetLocation = () => {
|
||||
Taro.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
// 这里可以调用地理编码API将坐标转换为地址
|
||||
// 暂时使用坐标信息
|
||||
setUseLocation(`经度:${res.longitude}, 纬度:${res.latitude}`)
|
||||
Taro.showToast({
|
||||
title: '位置获取成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '位置获取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
// 查看我的礼品卡
|
||||
const handleViewMyGifts = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/user/gift/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 转换礼品卡数据
|
||||
const transformGiftData = (gift: ShopGift) => {
|
||||
return {
|
||||
id: gift.id || 0,
|
||||
name: gift.name || '礼品卡',
|
||||
description: gift.description,
|
||||
code: gift.code,
|
||||
goodsImage: gift.goodsImage,
|
||||
faceValue: gift.faceValue,
|
||||
type: gift.type,
|
||||
useStatus: gift.useStatus,
|
||||
expireTime: gift.expireTime,
|
||||
useTime: gift.useTime,
|
||||
useLocation: gift.useLocation,
|
||||
contactInfo: gift.contactInfo,
|
||||
showCode: false,
|
||||
showUseBtn: false,
|
||||
showDetailBtn: false,
|
||||
theme: 'gold' as const
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex justify-center items-center h-screen">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (!gift) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="flex flex-col justify-center items-center h-screen">
|
||||
<Text className="text-gray-500 mb-4">礼品卡不存在</Text>
|
||||
<Button onClick={handleBack}>返回</Button>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
{/* 自定义导航栏 */}
|
||||
<View className="flex items-center justify-between p-4 bg-white border-b border-gray-100">
|
||||
<View className="flex items-center" onClick={handleBack}>
|
||||
<ArrowLeft size="20" />
|
||||
<Text className="ml-2 text-lg">使用礼品卡</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!useSuccess ? (
|
||||
<>
|
||||
{/* 礼品卡信息 */}
|
||||
<View className="mx-4 mt-4">
|
||||
<GiftCard {...transformGiftData(gift)} />
|
||||
</View>
|
||||
|
||||
{/* 使用表单 */}
|
||||
<View className="bg-white mx-4 mt-4 p-4 rounded-xl">
|
||||
<Text className="font-semibold mb-4 text-gray-800">使用信息</Text>
|
||||
|
||||
{/* 使用地址(实物礼品卡必填) */}
|
||||
{gift.type === 10 && (
|
||||
<View className="mb-4">
|
||||
<Text className="text-gray-700 mb-2">使用地址 *</Text>
|
||||
<View className="flex gap-2">
|
||||
<Input
|
||||
placeholder="请输入使用地址"
|
||||
value={useLocation}
|
||||
onChange={setUseLocation}
|
||||
className="flex-1 border border-gray-200 rounded-lg"
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Location />}
|
||||
onClick={handleGetLocation}
|
||||
>
|
||||
定位
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 虚拟礼品卡和服务礼品卡的地址选填 */}
|
||||
{gift.type !== 10 && (
|
||||
<View className="mb-4">
|
||||
<Text className="text-gray-700 mb-2">使用地址(选填)</Text>
|
||||
<Input
|
||||
placeholder="请输入使用地址"
|
||||
value={useLocation}
|
||||
onChange={setUseLocation}
|
||||
className="border border-gray-200 rounded-lg"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 使用备注 */}
|
||||
<View className="mb-4">
|
||||
<Text className="text-gray-700 mb-2">使用备注(选填)</Text>
|
||||
<TextArea
|
||||
placeholder="请输入使用备注"
|
||||
value={useNote}
|
||||
onChange={setUseNote}
|
||||
rows={3}
|
||||
className="border border-gray-200 rounded-lg"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<View className="bg-yellow-50 p-3 rounded-lg border border-yellow-200 mb-4">
|
||||
<Text className="text-yellow-800 text-sm">
|
||||
{gift.type === 10 && '💡 实物礼品卡使用后请到指定地址领取商品'}
|
||||
{gift.type === 20 && '💡 虚拟礼品卡使用后将自动发放到您的账户'}
|
||||
{gift.type === 30 && '💡 服务礼品卡使用后请联系客服预约服务时间'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
onClick={handleUseGift}
|
||||
>
|
||||
确认使用
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
/* 使用成功页面 */
|
||||
<View className="flex flex-col items-center justify-center px-4" style={{height: '600px'}}>
|
||||
<View className="text-center">
|
||||
<View className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
{/*<Voucher size="40" className="text-green-600" />*/}
|
||||
</View>
|
||||
|
||||
<Text className="text-2xl font-bold text-gray-900 mb-2">使用成功!</Text>
|
||||
<Text className="text-gray-600 mb-6">
|
||||
{gift.type === 10 && '请到指定地址领取您的商品'}
|
||||
{gift.type === 20 && '虚拟商品已发放到您的账户'}
|
||||
{gift.type === 30 && '请联系客服预约服务时间'}
|
||||
</Text>
|
||||
|
||||
{gift.contactInfo && (
|
||||
<View className="bg-blue-50 p-4 rounded-lg mb-6 border border-blue-200">
|
||||
<Text className="text-blue-800 font-semibold mb-1">客服联系方式</Text>
|
||||
<Text className="text-blue-700">{gift.contactInfo}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="flex flex-col gap-3 w-full max-w-xs">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={handleViewMyGifts}
|
||||
>
|
||||
查看我的礼品卡
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GiftCardUse;
|
||||
Reference in New Issue
Block a user