feat(admin): 从文章详情页面改为文章管理页面
- 修改页面配置,设置新的导航栏标题和样式 - 重新设计页面布局,增加搜索栏、文章列表和操作按钮 - 添加文章搜索、分页加载和删除功能 - 优化文章列表项的样式和交互 - 新增礼品卡相关API和组件 - 更新优惠券组件,增加到期提醒和筛选功能
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '文章详情'
|
navigationBarTitleText: '商品文章管理',
|
||||||
|
navigationBarTextStyle: 'black'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,53 +1,271 @@
|
|||||||
import Taro from '@tarojs/taro'
|
import {useState} from "react";
|
||||||
import {useEffect, useState} from 'react'
|
import Taro, {useDidShow} from '@tarojs/taro'
|
||||||
import {useRouter} from '@tarojs/taro'
|
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
|
||||||
import {Loading} from '@nutui/nutui-react-taro'
|
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
|
||||||
import {View, RichText} from '@tarojs/components'
|
import {View} from '@tarojs/components'
|
||||||
import {wxParse} from "@/utils/common";
|
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
import {pageCmsArticle, removeCmsArticle} from "@/api/cms/cmsArticle";
|
||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model"
|
import FixedButton from "@/components/FixedButton";
|
||||||
import Line from "@/components/Gap";
|
import dayjs from "dayjs";
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
function Detail() {
|
const ArticleArticleManage = () => {
|
||||||
const {params} = useRouter();
|
const [list, setList] = useState<CmsArticle[]>([])
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState(false)
|
||||||
// 文章详情
|
// const [refreshing, setRefreshing] = useState(false)
|
||||||
const [item, setItem] = useState<CmsArticle>()
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const reload = async () => {
|
const [searchValue, setSearchValue] = useState('')
|
||||||
const item = await getCmsArticle(Number(params.id))
|
const [page, setPage] = useState(1)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
|
||||||
if (item) {
|
const reload = async (isRefresh = false) => {
|
||||||
item.content = wxParse(item.content)
|
if (isRefresh) {
|
||||||
setItem(item)
|
setPage(1)
|
||||||
Taro.setNavigationBarTitle({
|
setList([])
|
||||||
title: `${item?.categoryName}`
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
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 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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// 加载更多
|
||||||
reload().then(() => {
|
const loadMore = async () => {
|
||||||
setLoading(false)
|
if (!loading && hasMore) {
|
||||||
|
await reload(false) // 不刷新,追加数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
reload(true).then()
|
||||||
});
|
});
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Loading className={'px-2'}>加载中</Loading>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'bg-white'}>
|
<ConfigProvider>
|
||||||
<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="py-2">
|
||||||
<View className={'content p-4'}>
|
<SearchBar
|
||||||
<RichText nodes={item?.content}/>
|
placeholder="搜索关键词"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Line height={44}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Detail
|
{/* 统计信息 */}
|
||||||
|
{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;
|
bmUsers?: number;
|
||||||
// 用户ID
|
// 用户ID
|
||||||
userId?: number;
|
userId?: number;
|
||||||
// 商户ID
|
|
||||||
merchantId?: number;
|
|
||||||
// 项目ID
|
// 项目ID
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
// 语言
|
// 语言
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export interface ShopCoupon {
|
|||||||
limitPerUser?: number;
|
limitPerUser?: number;
|
||||||
// 是否启用(0禁用 1启用)
|
// 是否启用(0禁用 1启用)
|
||||||
enabled?: string;
|
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",
|
"help/index",
|
||||||
"about/index",
|
"about/index",
|
||||||
"wallet/wallet",
|
"wallet/wallet",
|
||||||
"coupon/coupon",
|
// "coupon/index",
|
||||||
"points/points"
|
// "points/points",
|
||||||
|
"gift/index"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -54,15 +55,17 @@ export default defineAppConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "shop",
|
"root": "shop",
|
||||||
"pages": [
|
"pages": ['category/index',
|
||||||
'category/index',
|
|
||||||
'orderDetail/index',
|
'orderDetail/index',
|
||||||
'goodsDetail/index',
|
'goodsDetail/index',
|
||||||
'orderConfirm/index',
|
'orderConfirm/index',
|
||||||
'orderConfirmCart/index',
|
'orderConfirmCart/index',
|
||||||
'search/index',
|
'search/index']
|
||||||
'shopArticle/index',
|
},
|
||||||
'shopArticle/add'
|
{
|
||||||
|
"root": "admin",
|
||||||
|
"pages": [
|
||||||
|
"article/index",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 120px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
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;
|
background: #fff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
@@ -15,7 +21,7 @@
|
|||||||
|
|
||||||
.coupon-left {
|
.coupon-left {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 100px;
|
width: 110px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -24,23 +30,23 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.theme-red {
|
&.theme-red {
|
||||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-orange {
|
&.theme-orange {
|
||||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-blue {
|
&.theme-blue {
|
||||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-purple {
|
&.theme-purple {
|
||||||
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-green {
|
&.theme-green {
|
||||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-wrapper {
|
.amount-wrapper {
|
||||||
@@ -49,22 +55,23 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.currency {
|
.currency {
|
||||||
font-size: 24px;
|
font-size: 28px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
font-size: 30px;
|
font-size: 36px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition {
|
.condition {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
margin-top: 2px;
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,21 +119,23 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
|
|
||||||
.coupon-info {
|
.coupon-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.coupon-title {
|
.coupon-title {
|
||||||
font-size: 28px;
|
font-size: 32px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 6px;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-validity {
|
.coupon-validity {
|
||||||
font-size: 24px;
|
font-size: 26px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,38 +145,45 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.coupon-btn {
|
.coupon-btn {
|
||||||
min-width: 48px;
|
min-width: 120px;
|
||||||
height: 24px;
|
height: 60px;
|
||||||
border-radius: 12px;
|
border-radius: 30px;
|
||||||
font-size: 24px;
|
font-size: 26px;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
&.theme-red {
|
&.theme-red {
|
||||||
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-orange {
|
&.theme-orange {
|
||||||
background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
|
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-blue {
|
&.theme-blue {
|
||||||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-purple {
|
&.theme-purple {
|
||||||
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
|
background: linear-gradient(135deg, #ab47bc 0%, #9c27b0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-green {
|
&.theme-green {
|
||||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 24px;
|
font-size: 26px;
|
||||||
color: #6b7280;
|
color: #9ca3af;
|
||||||
padding: 4px 8px;
|
padding: 8px 12px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,41 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
|||||||
|
|
||||||
// 获取使用条件文本
|
// 获取使用条件文本
|
||||||
const getConditionText = () => {
|
const getConditionText = () => {
|
||||||
if (type === 3) return '无门槛'
|
if (type === 3) return '免费使用' // 免费券
|
||||||
if (minAmount && minAmount > 0) {
|
if (minAmount && minAmount > 0) {
|
||||||
return `满${minAmount}可用`
|
return `满${minAmount}元可用`
|
||||||
}
|
}
|
||||||
return '无门槛'
|
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) => {
|
const formatDate = (dateStr?: string) => {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
@@ -108,8 +136,8 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
|||||||
{/* 左侧金额区域 */}
|
{/* 左侧金额区域 */}
|
||||||
<View className={`coupon-left ${themeClass}`}>
|
<View className={`coupon-left ${themeClass}`}>
|
||||||
<View className="amount-wrapper">
|
<View className="amount-wrapper">
|
||||||
<Text className="currency">¥</Text>
|
{type !== 3 && <Text className="currency">¥</Text>}
|
||||||
<Text className="amount">{amount}</Text>
|
<Text className="amount">{formatAmount()}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="condition">
|
<View className="condition">
|
||||||
{getConditionText()}
|
{getConditionText()}
|
||||||
@@ -130,7 +158,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
|||||||
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')}
|
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')}
|
||||||
</View>
|
</View>
|
||||||
<View className="coupon-validity">
|
<View className="coupon-validity">
|
||||||
有效期:{getValidityText()}
|
{formatValidityPeriod()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -151,7 +179,7 @@ const CouponCard: React.FC<CouponCardProps> = ({
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={onUse}
|
onClick={onUse}
|
||||||
>
|
>
|
||||||
使用
|
立即使用
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{status !== 0 && (
|
{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 React from 'react'
|
||||||
import { View } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Cell, Button } from '@nutui/nutui-react-taro'
|
||||||
import Taro from '@tarojs/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 (
|
return (
|
||||||
<View className="min-h-screen" style={{
|
<View className="p-4">
|
||||||
paddingTop: '20px',
|
<Text className="text-lg font-bold mb-4">分销中心</Text>
|
||||||
background: 'linear-gradient(180deg, #60A5FA 0%, #3B82F6 50%, #1D4ED8 100%)'
|
|
||||||
}}>
|
<Cell.Group>
|
||||||
<UserCard />
|
<Cell
|
||||||
<EarningsCard onWithdraw={handleWithdraw} />
|
title="我的团队"
|
||||||
<FunctionMenu />
|
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>
|
</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 React from 'react'
|
||||||
import { View, Image } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import { Cell, Empty } from '@nutui/nutui-react-taro'
|
||||||
import { Empty, Tabs, TabPane } from '@nutui/nutui-react-taro'
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
interface DistributionOrder {
|
const DealerOrders: React.FC = () => {
|
||||||
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 (
|
return (
|
||||||
<View className="distribution-orders-page">
|
<View className="p-4">
|
||||||
<View className="loading-container">
|
<Text className="text-lg font-bold mb-4">分销订单</Text>
|
||||||
<View className="text-center text-gray-500">加载中...</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<Empty description="暂无分销订单" />
|
||||||
<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="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>
|
|
||||||
</View>
|
</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 React from 'react'
|
||||||
import { View, Canvas, Image } from '@tarojs/components'
|
import { View, Text, Image } from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import { Cell, Button } from '@nutui/nutui-react-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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const DealerQrcode: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<View className="promotion-qrcode-page">
|
<View className="p-4">
|
||||||
{/* 用户信息卡片 */}
|
<Text className="text-lg font-bold mb-4">推广二维码</Text>
|
||||||
<View className="user-card">
|
|
||||||
<Image className="user-avatar" src={userInfo.avatar} />
|
<View className="text-center">
|
||||||
<View className="user-info">
|
<View className="bg-gray-100 w-48 h-48 mx-auto mb-4 flex items-center justify-center">
|
||||||
<View className="user-name">{userInfo.nickname}</View>
|
<Text className="text-gray-500">二维码占位</Text>
|
||||||
<View className="invite-code">邀请码:{userInfo.inviteCode}</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 二维码展示区域 */}
|
<Text className="text-sm text-gray-600 mb-4">
|
||||||
<View className="qrcode-container">
|
扫描二维码或分享链接邀请好友
|
||||||
<View className="qrcode-card">
|
</Text>
|
||||||
<View className="qrcode-header">
|
|
||||||
<View className="title">我的专属推广二维码</View>
|
|
||||||
<View className="subtitle">扫码注册成为我的下级</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="qrcode-wrapper">
|
<Button type="primary" className="mb-2">
|
||||||
{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>
|
</Button>
|
||||||
<Button
|
|
||||||
className="action-btn secondary"
|
|
||||||
onClick={handleShareQRCode}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
分享二维码
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="action-btn tertiary"
|
|
||||||
onClick={handleRefreshQRCode}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
刷新二维码
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 使用说明 */}
|
<Button>
|
||||||
<View className="usage-tips">
|
分享链接
|
||||||
<View className="tips-title">使用说明</View>
|
</Button>
|
||||||
<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>
|
|
||||||
</View>
|
</View>
|
||||||
</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 React from 'react'
|
||||||
import { View } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import { Empty } from '@nutui/nutui-react-taro'
|
||||||
import { Avatar, Empty, Tabs, TabPane } from '@nutui/nutui-react-taro'
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
interface TeamMember {
|
const DealerTeam: React.FC = () => {
|
||||||
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 (
|
return (
|
||||||
<View className="my-team-page">
|
<View className="p-4">
|
||||||
<View className="loading-container">
|
<Text className="text-lg font-bold mb-4">我的团队</Text>
|
||||||
<View className="text-center text-gray-500">加载中...</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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="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="暂无团队成员" />
|
<Empty description="暂无团队成员" />
|
||||||
</View>
|
</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>
|
|
||||||
</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 React from 'react'
|
||||||
import { View } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import { Cell, Button, Form, Input } from '@nutui/nutui-react-taro'
|
||||||
import { Empty } from '@nutui/nutui-react-taro'
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
interface WithdrawRecord {
|
const DealerWithdraw: React.FC = () => {
|
||||||
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 (
|
return (
|
||||||
<View className="withdraw-detail-page">
|
<View className="p-4">
|
||||||
<View className="loading-container">
|
<Text className="text-lg font-bold mb-4">提现申请</Text>
|
||||||
<View className="text-center text-gray-500">加载中...</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<Form>
|
||||||
<View className="withdraw-detail-page">
|
<Cell.Group>
|
||||||
{records.length === 0 ? (
|
<Form.Item name="amount" label="提现金额" required>
|
||||||
<View className="empty-container">
|
<Input placeholder="请输入提现金额" type="number" />
|
||||||
<Empty description="暂无提现记录" />
|
</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>
|
||||||
) : (
|
</Form>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WithdrawDetail
|
export default DealerWithdraw
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function UserCard() {
|
|||||||
const [roleName, setRoleName] = useState<string>('注册用户')
|
const [roleName, setRoleName] = useState<string>('注册用户')
|
||||||
const [couponCount, setCouponCount] = useState(0)
|
const [couponCount, setCouponCount] = useState(0)
|
||||||
const [pointsCount, setPointsCount] = useState(0)
|
const [pointsCount, setPointsCount] = useState(0)
|
||||||
|
const [giftCount, setGiftCount] = useState(0)
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,6 +53,11 @@ function UserCard() {
|
|||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
console.error('Points stats error:', error)
|
console.error('Points stats error:', error)
|
||||||
})
|
})
|
||||||
|
// 加载礼品劵数量
|
||||||
|
setGiftCount(0)
|
||||||
|
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
|
||||||
|
// setGiftCount(res.count || 0)
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
@@ -85,7 +91,7 @@ function UserCard() {
|
|||||||
}
|
}
|
||||||
// 判断身份
|
// 判断身份
|
||||||
const roleName = Taro.getStorageSync('RoleName');
|
const roleName = Taro.getStorageSync('RoleName');
|
||||||
if(roleName){
|
if (roleName) {
|
||||||
setRoleName(roleName)
|
setRoleName(roleName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +159,7 @@ function UserCard() {
|
|||||||
TenantId
|
TenantId
|
||||||
},
|
},
|
||||||
success: function (res) {
|
success: function (res) {
|
||||||
if(res.data.code == 1){
|
if (res.data.code == 1) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: res.data.message,
|
title: res.data.message,
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -216,15 +222,23 @@ function UserCard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex justify-around mt-1'}>
|
<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-sm text-gray-500'}>余额</span>
|
||||||
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
|
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
|
||||||
</div>
|
</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-sm text-gray-500'}>优惠券</span>
|
||||||
<span className={'text-xl'}>{couponCount}</span>
|
<span className={'text-xl'}>{couponCount}</span>
|
||||||
</div>
|
</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-sm text-gray-500'}>积分</span>
|
||||||
<span className={'text-xl'}>{pointsCount}</span>
|
<span className={'text-xl'}>{pointsCount}</span>
|
||||||
</div>
|
</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({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '商品文章管理',
|
navigationBarTitleText: '新增收货地址',
|
||||||
navigationBarTextStyle: 'black'
|
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