diff --git a/config/env.ts b/config/env.ts index 44ddb26..c92883b 100644 --- a/config/env.ts +++ b/config/env.ts @@ -2,7 +2,7 @@ export const ENV_CONFIG = { // 开发环境 development: { - API_BASE_URL: 'http://127.0.0.1:9200/api', + API_BASE_URL: 'https://cms-api.websoft.top/api', APP_NAME: '开发环境', DEBUG: 'true', }, diff --git a/src/api/invite/index.ts b/src/api/invite/index.ts new file mode 100644 index 0000000..cd1ffe8 --- /dev/null +++ b/src/api/invite/index.ts @@ -0,0 +1,239 @@ +import request from '@/utils/request'; +import type { ApiResult, PageResult } from '@/api/index'; +import { SERVER_API_URL } from '@/utils/server'; + +/** + * 小程序码生成参数 + */ +export interface MiniProgramCodeParam { + // 小程序页面路径 + page?: string; + // 场景值,最大32个可见字符 + scene: string; + // 二维码宽度,单位 px,最小 280px,最大 1280px + width?: number; + // 是否检查页面是否存在 + checkPath?: boolean; + // 环境版本 + envVersion?: 'release' | 'trial' | 'develop'; +} + +/** + * 邀请关系参数 + */ +export interface InviteRelationParam { + // 邀请人ID + inviterId: number; + // 被邀请人ID + inviteeId: number; + // 邀请来源 + source: string; + // 场景值 + scene?: string; + // 邀请时间 + inviteTime?: string; +} + +/** + * 邀请统计数据 + */ +export interface InviteStats { + // 总邀请数 + totalInvites: number; + // 成功注册数 + successfulRegistrations: number; + // 转化率 + conversionRate: number; + // 今日邀请数 + todayInvites: number; + // 本月邀请数 + monthlyInvites: number; + // 邀请来源统计 + sourceStats: InviteSourceStat[]; +} + +/** + * 邀请来源统计 + */ +export interface InviteSourceStat { + source: string; + count: number; + successCount: number; + conversionRate: number; +} + +/** + * 邀请记录 + */ +export interface InviteRecord { + id?: number; + inviterId?: number; + inviteeId?: number; + inviterName?: string; + inviteeName?: string; + source?: string; + scene?: string; + status?: 'pending' | 'registered' | 'activated'; + inviteTime?: string; + registerTime?: string; + activateTime?: string; +} + +/** + * 邀请记录查询参数 + */ +export interface InviteRecordParam { + page?: number; + limit?: number; + inviterId?: number; + status?: string; + source?: string; + startTime?: string; + endTime?: string; +} + +/** + * 生成小程序码 + */ +export async function generateMiniProgramCode(data: MiniProgramCodeParam) { + const res = await request.post>( + SERVER_API_URL + '/invite/generate-miniprogram-code', + data + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 生成邀请小程序码 + */ +export async function generateInviteCode(inviterId: number, source: string = 'qrcode') { + const scene = `inviter=${inviterId}&source=${source}&t=${Date.now()}`; + + return generateMiniProgramCode({ + page: 'pages/index/index', + scene: scene, + width: 430, + checkPath: true, + envVersion: 'release' + }); +} + +/** + * 建立邀请关系 + */ +export async function createInviteRelation(data: InviteRelationParam) { + const res = await request.post>( + SERVER_API_URL + '/invite/create-relation', + data + ); + if (res.code === 0) { + return res.message; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 处理邀请场景值 + */ +export async function processInviteScene(scene: string, userId: number) { + const res = await request.post>( + SERVER_API_URL + '/invite/process-scene', + { scene, userId } + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 获取邀请统计数据 + */ +export async function getInviteStats(inviterId: number) { + const res = await request.get>( + SERVER_API_URL + `/invite/stats/${inviterId}` + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 分页查询邀请记录 + */ +export async function pageInviteRecords(params: InviteRecordParam) { + const res = await request.get>>( + SERVER_API_URL + '/invite/records/page', + params + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 获取我的邀请记录 + */ +export async function getMyInviteRecords(params: InviteRecordParam) { + const res = await request.get>>( + SERVER_API_URL + '/invite/my-records', + params + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 验证邀请码有效性 + */ +export async function validateInviteCode(scene: string) { + const res = await request.post>( + SERVER_API_URL + '/invite/validate-code', + { scene } + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 更新邀请状态 + */ +export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') { + const res = await request.put>( + SERVER_API_URL + `/invite/update-status/${inviteId}`, + { status } + ); + if (res.code === 0) { + return res.message; + } + return Promise.reject(new Error(res.message)); +} + +/** + * 获取邀请排行榜 + */ +export async function getInviteRanking(params?: { limit?: number; period?: 'day' | 'week' | 'month' }) { + const res = await request.get>>( + SERVER_API_URL + '/invite/ranking', + params + ); + if (res.code === 0) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/invite/model/index.ts b/src/api/invite/model/index.ts new file mode 100644 index 0000000..11aada9 --- /dev/null +++ b/src/api/invite/model/index.ts @@ -0,0 +1,279 @@ +import type { PageParam } from '@/api/index'; + +/** + * 邀请记录表 + */ +export interface InviteRecord { + // 主键ID + id?: number; + // 邀请人ID + inviterId?: number; + // 被邀请人ID + inviteeId?: number; + // 邀请人姓名 + inviterName?: string; + // 被邀请人姓名 + inviteeName?: string; + // 邀请来源 (qrcode, link, share等) + source?: string; + // 场景值 + scene?: string; + // 邀请状态: pending-待注册, registered-已注册, activated-已激活 + status?: 'pending' | 'registered' | 'activated'; + // 邀请时间 + inviteTime?: string; + // 注册时间 + registerTime?: string; + // 激活时间 + activateTime?: string; + // 备注 + comments?: string; + // 是否删除, 0否, 1是 + deleted?: number; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 邀请统计表 + */ +export interface InviteStats { + // 主键ID + id?: number; + // 邀请人ID + inviterId?: number; + // 统计日期 + statDate?: string; + // 总邀请数 + totalInvites?: number; + // 成功注册数 + successfulRegistrations?: number; + // 激活用户数 + activatedUsers?: number; + // 转化率 + conversionRate?: number; + // 今日邀请数 + todayInvites?: number; + // 本周邀请数 + weeklyInvites?: number; + // 本月邀请数 + monthlyInvites?: number; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 邀请来源统计表 + */ +export interface InviteSourceStats { + // 主键ID + id?: number; + // 邀请人ID + inviterId?: number; + // 来源类型 + source?: string; + // 来源名称 + sourceName?: string; + // 邀请数量 + inviteCount?: number; + // 成功数量 + successCount?: number; + // 转化率 + conversionRate?: number; + // 统计日期 + statDate?: string; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 小程序码记录表 + */ +export interface MiniProgramCode { + // 主键ID + id?: number; + // 邀请人ID + inviterId?: number; + // 场景值 + scene?: string; + // 小程序码URL + codeUrl?: string; + // 页面路径 + pagePath?: string; + // 二维码宽度 + width?: number; + // 环境版本 + envVersion?: string; + // 过期时间 + expireTime?: string; + // 使用次数 + useCount?: number; + // 最后使用时间 + lastUseTime?: string; + // 状态: active-有效, expired-过期, disabled-禁用 + status?: 'active' | 'expired' | 'disabled'; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 邀请记录搜索条件 + */ +export interface InviteRecordParam extends PageParam { + // 邀请人ID + inviterId?: number; + // 被邀请人ID + inviteeId?: number; + // 邀请状态 + status?: string; + // 邀请来源 + source?: string; + // 开始时间 + startTime?: string; + // 结束时间 + endTime?: string; + // 关键词搜索 + keywords?: string; +} + +/** + * 邀请统计搜索条件 + */ +export interface InviteStatsParam extends PageParam { + // 邀请人ID + inviterId?: number; + // 统计开始日期 + startDate?: string; + // 统计结束日期 + endDate?: string; +} + +/** + * 邀请来源统计搜索条件 + */ +export interface InviteSourceStatsParam extends PageParam { + // 邀请人ID + inviterId?: number; + // 来源类型 + source?: string; + // 统计开始日期 + startDate?: string; + // 统计结束日期 + endDate?: string; +} + +/** + * 小程序码搜索条件 + */ +export interface MiniProgramCodeParam extends PageParam { + // 邀请人ID + inviterId?: number; + // 状态 + status?: string; + // 场景值 + scene?: string; +} + +/** + * 邀请排行榜数据 + */ +export interface InviteRanking { + // 邀请人ID + inviterId?: number; + // 邀请人姓名 + inviterName?: string; + // 邀请人头像 + inviterAvatar?: string; + // 邀请数量 + inviteCount?: number; + // 成功数量 + successCount?: number; + // 转化率 + conversionRate?: number; + // 排名 + rank?: number; + // 奖励金额 + rewardAmount?: number; +} + +/** + * 邀请奖励配置 + */ +export interface InviteRewardConfig { + // 主键ID + id?: number; + // 奖励类型: register-注册奖励, activate-激活奖励, order-订单奖励 + rewardType?: string; + // 奖励名称 + rewardName?: string; + // 奖励金额 + rewardAmount?: number; + // 奖励积分 + rewardPoints?: number; + // 奖励优惠券ID + couponId?: number; + // 是否启用 + enabled?: boolean; + // 生效时间 + effectTime?: string; + // 失效时间 + expireTime?: string; + // 备注 + comments?: string; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} + +/** + * 邀请奖励记录 + */ +export interface InviteRewardRecord { + // 主键ID + id?: number; + // 邀请记录ID + inviteRecordId?: number; + // 邀请人ID + inviterId?: number; + // 被邀请人ID + inviteeId?: number; + // 奖励类型 + rewardType?: string; + // 奖励金额 + rewardAmount?: number; + // 奖励积分 + rewardPoints?: number; + // 优惠券ID + couponId?: number; + // 发放状态: pending-待发放, issued-已发放, failed-发放失败 + status?: 'pending' | 'issued' | 'failed'; + // 发放时间 + issueTime?: string; + // 失败原因 + failReason?: string; + // 租户id + tenantId?: number; + // 创建时间 + createTime?: string; + // 修改时间 + updateTime?: string; +} diff --git a/src/api/shop/shopDealerOrder/model/index.ts b/src/api/shop/shopDealerOrder/model/index.ts index 37017b8..68b4928 100644 --- a/src/api/shop/shopDealerOrder/model/index.ts +++ b/src/api/shop/shopDealerOrder/model/index.ts @@ -43,5 +43,9 @@ export interface ShopDealerOrder { */ export interface ShopDealerOrderParam extends PageParam { id?: number; + firstUserId?: number; + secondUserId?: number; + thirdUserId?: number; + userId?: number; keywords?: string; } diff --git a/src/api/shop/shopDealerReferee/model/index.ts b/src/api/shop/shopDealerReferee/model/index.ts index 258c004..61c1e49 100644 --- a/src/api/shop/shopDealerReferee/model/index.ts +++ b/src/api/shop/shopDealerReferee/model/index.ts @@ -1,4 +1,4 @@ -import type { PageParam } from '@/api/index'; +import type { PageParam } from '@/api'; /** * 分销商推荐关系表 @@ -25,5 +25,6 @@ export interface ShopDealerReferee { */ export interface ShopDealerRefereeParam extends PageParam { id?: number; + dealerId?: number; keywords?: string; } diff --git a/src/api/shop/shopDealerWithdraw/model/index.ts b/src/api/shop/shopDealerWithdraw/model/index.ts index 3193e62..d53db44 100644 --- a/src/api/shop/shopDealerWithdraw/model/index.ts +++ b/src/api/shop/shopDealerWithdraw/model/index.ts @@ -43,5 +43,6 @@ export interface ShopDealerWithdraw { */ export interface ShopDealerWithdrawParam extends PageParam { id?: number; + userId?: number; keywords?: string; } diff --git a/src/app.config.ts b/src/app.config.ts index a1be3ab..47fbce0 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -56,6 +56,7 @@ export default defineAppConfig({ "orders/index", "team/index", "qrcode/index", + "invite-stats/index", "info" ] }, diff --git a/src/app.scss b/src/app.scss index 004581b..263b035 100644 --- a/src/app.scss +++ b/src/app.scss @@ -17,6 +17,27 @@ button { } } +// 去掉 Grid 组件的边框 +.no-border-grid { + .nut-grid-item { + border: none !important; + border-right: none !important; + border-bottom: none !important; + + &::after { + border: none !important; + } + } + + .nut-grid { + border: none !important; + + &::after { + border: none !important; + } + } +} + // 微信授权按钮的特殊样式 button[open-type="getPhoneNumber"] { background: none !important; diff --git a/src/app.ts b/src/app.ts index cbf8ef3..418ba66 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,7 @@ import './app.scss' import {loginByOpenId} from "@/api/layout"; import {TenantId} from "@/config/app"; import {saveStorageByLoginUser} from "@/utils/server"; +import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation} from "@/utils/invite"; function App(props: { children: any; }) { const reload = () => { @@ -14,9 +15,21 @@ function App(props: { children: any; }) { loginByOpenId({ code: res.code, tenantId: TenantId - }).then(data => { + }).then(async data => { if (data) { saveStorageByLoginUser(data.access_token, data.user) + + // 处理邀请关系 + if (data.user?.userId) { + try { + const inviteSuccess = await handleInviteRelation(data.user.userId) + if (inviteSuccess) { + console.log('自动登录时邀请关系建立成功') + } + } catch (error) { + console.error('自动登录时处理邀请关系失败:', error) + } + } } }) } @@ -36,8 +49,41 @@ function App(props: { children: any; }) { // 对应 onShow useDidShow(() => { + // 处理小程序启动参数中的邀请信息 + const options = Taro.getLaunchOptionsSync() + handleLaunchOptions(options) }) + // 处理启动参数 + const handleLaunchOptions = (options: any) => { + try { + console.log('小程序启动参数:', options) + + // 解析邀请参数 + const inviteParams = parseInviteParams(options) + if (inviteParams) { + console.log('检测到邀请参数:', inviteParams) + + // 保存邀请参数到本地存储 + saveInviteParams(inviteParams) + + // 统计邀请来源 + trackInviteSource(inviteParams.source || 'unknown', parseInt(inviteParams.inviter || '0')) + + // 显示邀请提示 + setTimeout(() => { + Taro.showToast({ + title: '检测到邀请信息', + icon: 'success', + duration: 2000 + }) + }, 1000) + } + } catch (error) { + console.error('处理启动参数失败:', error) + } + } + // 对应 onHide useDidHide(() => { }) diff --git a/src/dealer/index.scss b/src/dealer/index.scss index e123fea..e69de29 100644 --- a/src/dealer/index.scss +++ b/src/dealer/index.scss @@ -1,8 +0,0 @@ -/* 添加这段样式后,Primary Button 会变成绿色 */ -:root { - --nutui-color-primary: green; - --nutui-color-primary-stop1: green; - --nutui-color-primary-stop2: green; - // 间隔线/容错线,用于结构或信息分割 - --nutui-black-2: rgba(255, 0, 0, 0.08); -} diff --git a/src/dealer/index.tsx b/src/dealer/index.tsx index e4be766..cec6dbe 100644 --- a/src/dealer/index.tsx +++ b/src/dealer/index.tsx @@ -7,7 +7,8 @@ import { Dongdong, ArrowRight, Purse, - People + People, + Chart } from '@nutui/icons-react-taro' import {useDealerUser} from '@/hooks/useDealerUser' import { useThemeStyles } from '@/hooks/useTheme' @@ -69,7 +70,7 @@ const DealerIndex: React.FC = () => { } return ( - + {/*头部信息*/} {dealerUser && ( @@ -162,9 +163,9 @@ const DealerIndex: React.FC = () => { {/* 团队统计 */} {dealerUser && ( - + - 我的团队 + 我的邀请 navigateToPage('/dealer/team/index')} @@ -200,7 +201,15 @@ const DealerIndex: React.FC = () => { 分销工具 - + navigateToPage('/dealer/orders/index')}> @@ -209,30 +218,68 @@ const DealerIndex: React.FC = () => { - navigateToPage('/dealer/withdraw/index')}> + navigateToPage('/dealer/withdraw/index')}> - 提现申请 - navigateToPage('/dealer/team/index')}> + navigateToPage('/dealer/team/index')}> - 我的团队 - navigateToPage('/dealer/qrcode/index')}> + navigateToPage('/dealer/qrcode/index')}> - 推广二维码 + + + + + {/* 第二行功能 */} + + navigateToPage('/dealer/invite-stats/index')}> + + + + + + + + {/* 预留其他功能位置 */} + + + + + + + + + + + + + + + + + + diff --git a/src/dealer/invite-stats/index.config.ts b/src/dealer/invite-stats/index.config.ts new file mode 100644 index 0000000..246a9aa --- /dev/null +++ b/src/dealer/invite-stats/index.config.ts @@ -0,0 +1,7 @@ +export default definePageConfig({ + navigationBarTitleText: '邀请统计', + navigationBarBackgroundColor: '#ffffff', + navigationBarTextStyle: 'black', + backgroundColor: '#f5f5f5', + enablePullDownRefresh: true +}) diff --git a/src/dealer/invite-stats/index.tsx b/src/dealer/invite-stats/index.tsx new file mode 100644 index 0000000..9a6a310 --- /dev/null +++ b/src/dealer/invite-stats/index.tsx @@ -0,0 +1,343 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { View, Text } from '@tarojs/components' +import { + Empty, + Tabs, + Progress, + Loading, + PullToRefresh, + Card, + Button, + DatePicker +} from '@nutui/nutui-react-taro' +import { + User, + Star, + TrendingUp, + Calendar, + Share, + Target, + Award +} from '@nutui/icons-react-taro' +import Taro from '@tarojs/taro' +import { useDealerUser } from '@/hooks/useDealerUser' +import { + getInviteStats, + getMyInviteRecords, + getInviteRanking +} from '@/api/invite' +import type { + InviteStats, + InviteRecord, + InviteRanking +} from '@/api/invite' +import { businessGradients } from '@/styles/gradients' + +const InviteStatsPage: React.FC = () => { + const [activeTab, setActiveTab] = useState('stats') + const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const [inviteStats, setInviteStats] = useState(null) + const [inviteRecords, setInviteRecords] = useState([]) + const [ranking, setRanking] = useState([]) + const [dateRange, setDateRange] = useState('month') + const { dealerUser } = useDealerUser() + + // 获取邀请统计数据 + const fetchInviteStats = useCallback(async () => { + if (!dealerUser?.userId) return + + try { + setLoading(true) + const stats = await getInviteStats(dealerUser.userId) + setInviteStats(stats) + } catch (error) { + console.error('获取邀请统计失败:', error) + Taro.showToast({ + title: '获取统计数据失败', + icon: 'error' + }) + } finally { + setLoading(false) + } + }, [dealerUser?.userId]) + + // 获取邀请记录 + const fetchInviteRecords = useCallback(async () => { + if (!dealerUser?.userId) return + + try { + const result = await getMyInviteRecords({ + page: 1, + limit: 50, + inviterId: dealerUser.userId + }) + setInviteRecords(result?.list || []) + } catch (error) { + console.error('获取邀请记录失败:', error) + } + }, [dealerUser?.userId]) + + // 获取邀请排行榜 + const fetchRanking = useCallback(async () => { + try { + const result = await getInviteRanking({ + limit: 20, + period: dateRange as 'day' | 'week' | 'month' + }) + setRanking(result || []) + } catch (error) { + console.error('获取排行榜失败:', error) + } + }, [dateRange]) + + // 刷新数据 + const handleRefresh = async () => { + setRefreshing(true) + await Promise.all([ + fetchInviteStats(), + fetchInviteRecords(), + fetchRanking() + ]) + setRefreshing(false) + } + + // 初始化数据 + useEffect(() => { + if (dealerUser?.userId) { + fetchInviteStats() + fetchInviteRecords() + fetchRanking() + } + }, [fetchInviteStats, fetchInviteRecords, fetchRanking]) + + // 获取状态显示文本 + const getStatusText = (status: string) => { + const statusMap: Record = { + 'pending': '待注册', + 'registered': '已注册', + 'activated': '已激活' + } + return statusMap[status] || status + } + + // 获取状态颜色 + const getStatusColor = (status: string) => { + const colorMap: Record = { + 'pending': 'text-orange-500', + 'registered': 'text-blue-500', + 'activated': 'text-green-500' + } + return colorMap[status] || 'text-gray-500' + } + + // 渲染统计概览 + const renderStatsOverview = () => ( + + {/* 核心数据卡片 */} + + + 邀请概览 + {loading ? ( + + + + ) : inviteStats ? ( + + + + + {inviteStats.totalInvites || 0} + + 总邀请数 + + + + + + {inviteStats.successfulRegistrations || 0} + + 成功注册 + + + + + + {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'} + + 转化率 + + + + + + {inviteStats.todayInvites || 0} + + 今日邀请 + + + ) : ( + + 暂无统计数据 + + )} + + + + {/* 邀请来源分析 */} + {inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && ( + + + 邀请来源分析 + + {inviteStats.sourceStats.map((source, index) => ( + + + + {source.source} + + + {source.count} + + 转化率 {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'} + + + + ))} + + + + )} + + ) + + // 渲染邀请记录 + const renderInviteRecords = () => ( + + {inviteRecords.length > 0 ? ( + + {inviteRecords.map((record, index) => ( + + + + + {record.inviteeName || `用户${record.inviteeId}`} + + + {getStatusText(record.status || 'pending')} + + + + + 来源: {record.source || '未知'} + {record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''} + + + {record.registerTime && ( + + 注册时间: {new Date(record.registerTime).toLocaleString()} + + )} + + + ))} + + ) : ( + + )} + + ) + + // 渲染排行榜 + const renderRanking = () => ( + + + + + + + + + + {ranking.length > 0 ? ( + + {ranking.map((item, index) => ( + + + + {index < 3 ? ( + + ) : ( + {index + 1} + )} + + + + {item.inviterName} + + 邀请 {item.inviteCount} 人 · 转化率 {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'} + + + + {item.successCount} + + + ))} + + ) : ( + + )} + + ) + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + + return ( + + {/* 头部 */} + + + + + 邀请统计 + + 查看您的邀请效果和推广数据 + + + + + {/* 标签页 */} + + + + + + + + + {/* 内容区域 */} + + + {activeTab === 'stats' && renderStatsOverview()} + {activeTab === 'records' && renderInviteRecords()} + {activeTab === 'ranking' && renderRanking()} + + + + ) +} + +export default InviteStatsPage diff --git a/src/dealer/orders/index.tsx b/src/dealer/orders/index.tsx index 2407d09..1cc459b 100644 --- a/src/dealer/orders/index.tsx +++ b/src/dealer/orders/index.tsx @@ -1,60 +1,197 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { View, Text } from '@tarojs/components' -import { Empty, Tabs, Tag, PullToRefresh } from '@nutui/nutui-react-taro' +import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro' +import Taro from '@tarojs/taro' +import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder' +import { useDealerUser } from '@/hooks/useDealerUser' +import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model' + +interface OrderWithDetails extends ShopDealerOrder { + orderNo?: string + customerName?: string + totalCommission?: string + // 当前用户在此订单中的层级和佣金 + userLevel?: 1 | 2 | 3 + userCommission?: string +} const DealerOrders: React.FC = () => { const [activeTab, setActiveTab] = useState('0') - const [refreshing, setRefreshing] = useState(false) - - // 模拟订单数据 - const mockOrders = [ - { - id: '1', - orderNo: 'DD202412180001', - customerName: '张三', - amount: '299.00', - commission: '29.90', - status: 'completed', - createTime: '2024-12-18 10:30:00' - }, - { - id: '2', - orderNo: 'DD202412180002', - customerName: '李四', - amount: '599.00', - commission: '59.90', - status: 'pending', - createTime: '2024-12-18 14:20:00' - } - ] - - const getStatusText = (status: string) => { - switch (status) { - case 'completed': return '已完成' - case 'pending': return '待结算' - case 'cancelled': return '已取消' - default: return '未知' + const [loading, setLoading] = useState(false) + const [orders, setOrders] = useState([]) + const [statistics, setStatistics] = useState({ + totalOrders: 0, + totalCommission: '0.00', + pendingCommission: '0.00', + // 分层统计 + level1: { orders: 0, commission: '0.00' }, + level2: { orders: 0, commission: '0.00' }, + level3: { orders: 0, commission: '0.00' } + }) + + const { dealerUser } = useDealerUser() + + // 获取订单数据 - 查询当前用户作为各层级分销商的所有订单 + const fetchOrders = useCallback(async () => { + if (!dealerUser?.userId) return + + try { + setLoading(true) + + // 并行查询三个层级的订单 + const [level1Result, level2Result, level3Result] = await Promise.all([ + // 一级分销商订单 + pageShopDealerOrder({ + page: 1, + limit: 100, + firstUserId: dealerUser.userId + }), + // 二级分销商订单 + pageShopDealerOrder({ + page: 1, + limit: 100, + secondUserId: dealerUser.userId + }), + // 三级分销商订单 + pageShopDealerOrder({ + page: 1, + limit: 100, + thirdUserId: dealerUser.userId + }) + ]) + + const allOrders: OrderWithDetails[] = [] + const stats = { + totalOrders: 0, + totalCommission: '0.00', + pendingCommission: '0.00', + level1: { orders: 0, commission: '0.00' }, + level2: { orders: 0, commission: '0.00' }, + level3: { orders: 0, commission: '0.00' } + } + + // 处理一级分销订单 + if (level1Result?.list) { + const level1Orders = level1Result.list.map(order => ({ + ...order, + orderNo: `DD${order.orderId}`, + customerName: `用户${order.userId}`, + userLevel: 1 as const, + userCommission: order.firstMoney || '0.00', + totalCommission: ( + parseFloat(order.firstMoney || '0') + + parseFloat(order.secondMoney || '0') + + parseFloat(order.thirdMoney || '0') + ).toFixed(2) + })) + + allOrders.push(...level1Orders) + stats.level1.orders = level1Orders.length + stats.level1.commission = level1Orders.reduce((sum, order) => + sum + parseFloat(order.userCommission || '0'), 0 + ).toFixed(2) + } + + // 处理二级分销订单 + if (level2Result?.list) { + const level2Orders = level2Result.list.map(order => ({ + ...order, + orderNo: `DD${order.orderId}`, + customerName: `用户${order.userId}`, + userLevel: 2 as const, + userCommission: order.secondMoney || '0.00', + totalCommission: ( + parseFloat(order.firstMoney || '0') + + parseFloat(order.secondMoney || '0') + + parseFloat(order.thirdMoney || '0') + ).toFixed(2) + })) + + allOrders.push(...level2Orders) + stats.level2.orders = level2Orders.length + stats.level2.commission = level2Orders.reduce((sum, order) => + sum + parseFloat(order.userCommission || '0'), 0 + ).toFixed(2) + } + + // 处理三级分销订单 + if (level3Result?.list) { + const level3Orders = level3Result.list.map(order => ({ + ...order, + orderNo: `DD${order.orderId}`, + customerName: `用户${order.userId}`, + userLevel: 3 as const, + userCommission: order.thirdMoney || '0.00', + totalCommission: ( + parseFloat(order.firstMoney || '0') + + parseFloat(order.secondMoney || '0') + + parseFloat(order.thirdMoney || '0') + ).toFixed(2) + })) + + allOrders.push(...level3Orders) + stats.level3.orders = level3Orders.length + stats.level3.commission = level3Orders.reduce((sum, order) => + sum + parseFloat(order.userCommission || '0'), 0 + ).toFixed(2) + } + + // 去重(同一个订单可能在多个层级中出现) + const uniqueOrders = allOrders.filter((order, index, self) => + index === self.findIndex(o => o.orderId === order.orderId) + ) + + // 计算总统计 + stats.totalOrders = uniqueOrders.length + stats.totalCommission = ( + parseFloat(stats.level1.commission) + + parseFloat(stats.level2.commission) + + parseFloat(stats.level3.commission) + ).toFixed(2) + stats.pendingCommission = allOrders + .filter(order => order.isSettled === 0) + .reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0) + .toFixed(2) + + setOrders(uniqueOrders) + setStatistics(stats) + + } catch (error) { + console.error('获取分销订单失败:', error) + Taro.showToast({ + title: '获取订单失败', + icon: 'error' + }) + } finally { + setLoading(false) } + }, [dealerUser?.userId]) + + // 刷新数据 + const handleRefresh = async () => { + await fetchOrders() } - const getStatusColor = (status: string) => { - switch (status) { - case 'completed': return 'success' - case 'pending': return 'warning' - case 'cancelled': return 'danger' - default: return 'default' + // 初始化加载数据 + useEffect(() => { + if (dealerUser?.userId) { + fetchOrders().then() } + }, [fetchOrders]) + + const getStatusText = (isSettled?: number, isInvalid?: number) => { + if (isInvalid === 1) return '已失效' + if (isSettled === 1) return '已结算' + return '待结算' } - const handleRefresh = async () => { - setRefreshing(true) - // 模拟刷新 - setTimeout(() => { - setRefreshing(false) - }, 1000) + const getStatusColor = (isSettled?: number, isInvalid?: number) => { + if (isInvalid === 1) return 'danger' + if (isSettled === 1) return 'success' + return 'warning' } - const renderOrderItem = (order: any) => ( + const renderOrderItem = (order: OrderWithDetails) => ( @@ -64,19 +201,28 @@ const DealerOrders: React.FC = () => { 客户:{order.customerName} + {/* 显示用户在此订单中的层级 */} + + {order.userLevel === 1 && '一级分销'} + {order.userLevel === 2 && '二级分销'} + {order.userLevel === 3 && '三级分销'} + - - {getStatusText(order.status)} + + {getStatusText(order.isSettled, order.isInvalid)} - 订单金额:¥{order.amount} + 订单金额:¥{order.orderPrice || '0.00'} - 预计佣金:¥{order.commission} + 我的佣金:¥{order.userCommission} + + + 总佣金:¥{order.totalCommission} @@ -86,37 +232,92 @@ const DealerOrders: React.FC = () => { ) + // 根据状态和层级过滤订单 + const getFilteredOrders = (filter: string) => { + switch (filter) { + case '1': // 一级分销 + return orders.filter(order => order.userLevel === 1) + case '2': // 二级分销 + return orders.filter(order => order.userLevel === 2) + case '3': // 三级分销 + return orders.filter(order => order.userLevel === 3) + case '4': // 待结算 + return orders.filter(order => order.isSettled === 0 && order.isInvalid === 0) + case '5': // 已结算 + return orders.filter(order => order.isSettled === 1) + case '6': // 已失效 + return orders.filter(order => order.isInvalid === 1) + default: // 全部 + return orders + } + } + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + return ( {/* 统计卡片 */} - + {/* 总体统计 */} + - 2 + {statistics.totalOrders} 总订单 - ¥89.80 + ¥{statistics.totalCommission} 总佣金 - ¥29.90 + ¥{statistics.pendingCommission} 待结算 + + {/* 分层统计 */} + + 分层统计 + + + {statistics.level1.orders} + 一级订单 + ¥{statistics.level1.commission} + + + {statistics.level2.orders} + 二级订单 + ¥{statistics.level2.commission} + + + {statistics.level3.orders} + 三级订单 + ¥{statistics.level3.commission} + + + {/* 订单列表 */} setActiveTab}> - {mockOrders.length > 0 ? ( - mockOrders.map(renderOrderItem) + {loading ? ( + + + 加载中... + + ) : getFilteredOrders('0').length > 0 ? ( + getFilteredOrders('0').map(renderOrderItem) ) : ( )} @@ -124,15 +325,63 @@ const DealerOrders: React.FC = () => { - + + + {getFilteredOrders('1').length > 0 ? ( + getFilteredOrders('1').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('2').length > 0 ? ( + getFilteredOrders('2').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('3').length > 0 ? ( + getFilteredOrders('3').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('4').length > 0 ? ( + getFilteredOrders('4').map(renderOrderItem) + ) : ( + + )} + + + + - {mockOrders.filter(o => o.status === 'pending').map(renderOrderItem)} + {getFilteredOrders('5').length > 0 ? ( + getFilteredOrders('5').map(renderOrderItem) + ) : ( + + )} - + - {mockOrders.filter(o => o.status === 'completed').map(renderOrderItem)} + {getFilteredOrders('6').length > 0 ? ( + getFilteredOrders('6').map(renderOrderItem) + ) : ( + + )} diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index 8e4ff92..cb952d7 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -1,28 +1,370 @@ -import React from 'react' +import React, { useState, useEffect } from 'react' import { View, Text, Image } from '@tarojs/components' -import { Cell, Button } from '@nutui/nutui-react-taro' +import { Button, Loading } from '@nutui/nutui-react-taro' +import { Share, Download, Copy, QrCode } from '@nutui/icons-react-taro' +import Taro from '@tarojs/taro' +import { useDealerUser } from '@/hooks/useDealerUser' +import { generateInviteCode, getInviteStats } from '@/api/invite' +import type { InviteStats } from '@/api/invite' +import { businessGradients } from '@/styles/gradients' const DealerQrcode: React.FC = () => { + const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState('') + const [loading, setLoading] = useState(false) + const [inviteStats, setInviteStats] = useState(null) + const [statsLoading, setStatsLoading] = useState(false) + const { dealerUser } = useDealerUser() + + // 生成小程序码 + const generateMiniProgramCode = async () => { + if (!dealerUser?.userId) return + + try { + setLoading(true) + + // 生成邀请小程序码 + const codeUrl = await generateInviteCode(dealerUser.userId, 'qrcode') + + if (codeUrl) { + setMiniProgramCodeUrl(codeUrl) + } + } catch (error) { + console.error('生成小程序码失败:', error) + Taro.showToast({ + title: '生成小程序码失败', + icon: 'error' + }) + } finally { + setLoading(false) + } + } + + // 获取邀请统计数据 + const fetchInviteStats = async () => { + if (!dealerUser?.userId) return + + try { + setStatsLoading(true) + const stats = await getInviteStats(dealerUser.userId) + stats && setInviteStats(stats) + } catch (error) { + console.error('获取邀请统计失败:', error) + } finally { + setStatsLoading(false) + } + } + + // 初始化生成小程序码和获取统计数据 + useEffect(() => { + if (dealerUser?.userId) { + generateMiniProgramCode() + fetchInviteStats() + } + }, [dealerUser?.userId]) + + // 保存小程序码到相册 + const saveMiniProgramCode = async () => { + if (!miniProgramCodeUrl) { + Taro.showToast({ + title: '小程序码未生成', + icon: 'error' + }) + return + } + + try { + // 先下载图片到本地 + const res = await Taro.downloadFile({ + url: miniProgramCodeUrl + }) + + if (res.statusCode === 200) { + // 保存到相册 + await Taro.saveImageToPhotosAlbum({ + filePath: res.tempFilePath + }) + + Taro.showToast({ + title: '保存成功', + icon: 'success' + }) + } + } catch (error: any) { + if (error.errMsg?.includes('auth deny')) { + Taro.showModal({ + title: '提示', + content: '需要您授权保存图片到相册', + success: (res) => { + if (res.confirm) { + Taro.openSetting() + } + } + }) + } else { + Taro.showToast({ + title: '保存失败', + icon: 'error' + }) + } + } + } + + // 复制邀请信息 + const copyInviteInfo = () => { + if (!dealerUser?.userId) { + Taro.showToast({ + title: '用户信息未加载', + icon: 'error' + }) + return + } + + const inviteText = `🎉 邀请您加入我的团队! + +扫描小程序码或搜索"网宿小店"小程序,即可享受优质商品和服务! + +💰 成为我的下级分销商,一起赚取丰厚佣金 +🎁 新用户专享优惠等你来拿 + +邀请码:${dealerUser.userId} +快来加入我们吧!` + + Taro.setClipboardData({ + data: inviteText, + success: () => { + Taro.showToast({ + title: '邀请信息已复制', + icon: 'success' + }) + } + }) + } + + // 分享小程序码 + const shareMiniProgramCode = () => { + if (!dealerUser?.userId) { + Taro.showToast({ + title: '用户信息未加载', + icon: 'error' + }) + return + } + + // 小程序分享 + Taro.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'] + }) + } + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + return ( - - 推广二维码 - - - - 二维码占位 + + {/* 头部卡片 */} + + {/* 装饰背景 */} + + + + 我的邀请小程序码 + + 分享小程序码邀请好友,获得丰厚佣金奖励 + + + + + + {/* 小程序码展示区 */} + + + {loading ? ( + + + 生成中... + + ) : miniProgramCodeUrl ? ( + + + + ) : ( + + + 小程序码生成失败 + + + )} + + + 扫码加入我的团队 + + + 好友扫描小程序码即可直接进入小程序并建立邀请关系 + + + + + {/* 操作按钮 */} + + + + + + + + + {/* 推广说明 */} + + 推广说明 + + + + + 好友通过您的二维码或链接注册成为您的下级分销商 + + + + + + 好友购买商品时,您可获得相应层级的分销佣金 + + + + + + 支持三级分销,团队越大收益越多 + + + + + + {/* 邀请统计数据 */} + + 我的邀请数据 + {statsLoading ? ( + + + 加载中... + + ) : inviteStats ? ( + + + + + {inviteStats.totalInvites || 0} + + 总邀请数 + + + + {inviteStats.successfulRegistrations || 0} + + 成功注册 + + + + + + + {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'} + + 转化率 + + + + {inviteStats.todayInvites || 0} + + 今日邀请 + + + + {/* 邀请来源统计 */} + {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && ( + + 邀请来源分布 + + {inviteStats.sourceStats.map((source, index) => ( + + + + {source.source} + + + {source.count} + + {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'} + + + + ))} + + + )} + + ) : ( + + 暂无邀请数据 + + + )} - - - 扫描二维码或分享链接邀请好友 - - - - - ) diff --git a/src/dealer/team/index.tsx b/src/dealer/team/index.tsx index bd7c912..c305299 100644 --- a/src/dealer/team/index.tsx +++ b/src/dealer/team/index.tsx @@ -1,55 +1,144 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { View, Text } from '@tarojs/components' -import { Empty, Tabs, Avatar, Tag, Progress } from '@nutui/nutui-react-taro' +import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro' import { User, Star, StarFill } from '@nutui/icons-react-taro' +import Taro from '@tarojs/taro' +import { useDealerUser } from '@/hooks/useDealerUser' +import { listShopDealerReferee } from '@/api/shop/shopDealerReferee' +import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder' +import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model' + +interface TeamMemberWithStats extends ShopDealerReferee { + name?: string + avatar?: string + orderCount?: number + commission?: string + status?: 'active' | 'inactive' + subMembers?: number + joinTime?: string +} const DealerTeam: React.FC = () => { const [activeTab, setActiveTab] = useState('0') + const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const [teamMembers, setTeamMembers] = useState([]) + const [teamStats, setTeamStats] = useState({ + total: 0, + firstLevel: 0, + secondLevel: 0, + thirdLevel: 0, + monthlyCommission: '0.00' + }) + + const { dealerUser } = useDealerUser() + + // 获取团队数据 + const fetchTeamData = useCallback(async () => { + if (!dealerUser?.userId) return + + try { + setLoading(true) + + // 获取团队成员关系 + const refereeResult = await listShopDealerReferee({ + dealerId: dealerUser.userId + }) + + if (refereeResult) { + // 处理团队成员数据 + const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({ + ...member, + name: `用户${member.userId}`, + avatar: '', + orderCount: 0, + commission: '0.00', + status: 'active' as const, + subMembers: 0, + joinTime: member.createTime + })) + + // 并行获取每个成员的订单统计 + const memberStats = await Promise.all( + processedMembers.map(async (member) => { + try { + const orderResult = await pageShopDealerOrder({ + page: 1, + limit: 100, + userId: member.userId + }) + + if (orderResult?.list) { + const orders = orderResult.list + const orderCount = orders.length + const commission = orders.reduce((sum, order) => { + const levelCommission = member.level === 1 ? order.firstMoney : + member.level === 2 ? order.secondMoney : + order.thirdMoney + return sum + parseFloat(levelCommission || '0') + }, 0).toFixed(2) + + // 判断活跃状态(30天内有订单为活跃) + const thirtyDaysAgo = new Date() + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30) + const hasRecentOrder = orders.some(order => + new Date(order.createTime || '') > thirtyDaysAgo + ) + + return { + ...member, + orderCount, + commission, + status: hasRecentOrder ? 'active' as const : 'inactive' as const + } + } + return member + } catch (error) { + console.error(`获取成员${member.userId}订单失败:`, error) + return member + } + }) + ) + + setTeamMembers(memberStats) - // 模拟团队数据 - const teamStats = { - total: 28, - firstLevel: 12, - secondLevel: 10, - thirdLevel: 6, - monthlyCommission: '2,580.50' + // 计算统计数据 + const stats = { + total: memberStats.length, + firstLevel: memberStats.filter(m => m.level === 1).length, + secondLevel: memberStats.filter(m => m.level === 2).length, + thirdLevel: memberStats.filter(m => m.level === 3).length, + monthlyCommission: memberStats.reduce((sum, member) => + sum + parseFloat(member.commission || '0'), 0 + ).toFixed(2) + } + + setTeamStats(stats) + } + } catch (error) { + console.error('获取团队数据失败:', error) + Taro.showToast({ + title: '获取团队数据失败', + icon: 'error' + }) + } finally { + setLoading(false) + } + }, [dealerUser?.userId]) + + // 刷新数据 + const handleRefresh = async () => { + setRefreshing(true) + await fetchTeamData() + setRefreshing(false) } - const teamMembers = [ - { - id: '1', - name: '张小明', - level: 1, - joinTime: '2024-11-15', - orderCount: 15, - commission: '580.50', - status: 'active', - avatar: '', - subMembers: 3 - }, - { - id: '2', - name: '李小红', - level: 1, - joinTime: '2024-12-01', - orderCount: 8, - commission: '320.00', - status: 'active', - avatar: '', - subMembers: 2 - }, - { - id: '3', - name: '王小华', - level: 2, - joinTime: '2024-12-10', - orderCount: 5, - commission: '150.00', - status: 'inactive', - avatar: '', - subMembers: 0 + // 初始化加载数据 + useEffect(() => { + if (dealerUser?.userId) { + fetchTeamData().then() } - ] + }, [fetchTeamData]) const getLevelColor = (level: number) => { switch (level) { @@ -69,7 +158,7 @@ const DealerTeam: React.FC = () => { } } - const renderMemberItem = (member: any) => ( + const renderMemberItem = (member: TeamMemberWithStats) => ( { {member.name} - {getLevelIcon(member.level)} + {getLevelIcon(Number(member.level))} {member.level}级 @@ -171,7 +260,7 @@ const DealerTeam: React.FC = () => { @@ -187,7 +276,7 @@ const DealerTeam: React.FC = () => { @@ -203,7 +292,7 @@ const DealerTeam: React.FC = () => { @@ -220,17 +309,38 @@ const DealerTeam: React.FC = () => { ) const renderMemberList = (level?: number) => ( - - {teamMembers - .filter(member => !level || member.level === level) - .map(renderMemberItem)} - - {teamMembers.filter(member => !level || member.level === level).length === 0 && ( - - )} - + + + {loading ? ( + + + 加载中... + + ) : teamMembers + .filter(member => !level || member.level === level) + .length > 0 ? ( + teamMembers + .filter(member => !level || member.level === level) + .map(renderMemberItem) + ) : ( + + )} + + ) + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + return ( setActiveTab}> diff --git a/src/dealer/withdraw/index.tsx b/src/dealer/withdraw/index.tsx index 865afea..eb671bd 100644 --- a/src/dealer/withdraw/index.tsx +++ b/src/dealer/withdraw/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react' +import React, { useState, useRef, useEffect, useCallback } from 'react' import { View, Text } from '@tarojs/components' import { Cell, @@ -9,63 +9,196 @@ import { Radio, Tabs, Tag, - Empty + Empty, + Loading, + PullToRefresh } from '@nutui/nutui-react-taro' import { Wallet } from '@nutui/icons-react-taro' import { businessGradients } from '@/styles/gradients' import Taro from '@tarojs/taro' +import { useDealerUser } from '@/hooks/useDealerUser' +import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw' +import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model' + +interface WithdrawRecordWithDetails extends ShopDealerWithdraw { + accountDisplay?: string +} const DealerWithdraw: React.FC = () => { const [activeTab, setActiveTab] = useState('0') const [selectedAccount, setSelectedAccount] = useState('') + const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [availableAmount, setAvailableAmount] = useState('0.00') + const [withdrawRecords, setWithdrawRecords] = useState([]) const formRef = useRef(null) - // 模拟可提现金额 - const availableAmount = '1,288.50' - - // 模拟提现记录 - const withdrawRecords = [ - { - id: '1', - amount: '500.00', - account: '尾号1234', - status: 'completed', - createTime: '2024-12-15 10:30:00', - completeTime: '2024-12-15 16:20:00' - }, - { - id: '2', - amount: '300.00', - account: '尾号1234', - status: 'pending', - createTime: '2024-12-18 09:15:00' + const { dealerUser } = useDealerUser() + + // 获取可提现余额 + const fetchBalance = useCallback(async () => { + try { + setAvailableAmount(dealerUser?.money || '0.00') + } catch (error) { + console.error('获取余额失败:', error) + } + }, []) + + // 获取提现记录 + const fetchWithdrawRecords = useCallback(async () => { + if (!dealerUser?.userId) return + + try { + setLoading(true) + const result = await pageShopDealerWithdraw({ + page: 1, + limit: 100, + userId: dealerUser.userId + }) + + if (result?.list) { + const processedRecords = result.list.map(record => ({ + ...record, + accountDisplay: getAccountDisplay(record) + })) + setWithdrawRecords(processedRecords) + } + } catch (error) { + console.error('获取提现记录失败:', error) + Taro.showToast({ + title: '获取提现记录失败', + icon: 'error' + }) + } finally { + setLoading(false) + } + }, [dealerUser?.userId]) + + // 格式化账户显示 + const getAccountDisplay = (record: ShopDealerWithdraw) => { + if (record.payType === 10) { + return '微信钱包' + } else if (record.payType === 20 && record.alipayAccount) { + return `支付宝(${record.alipayAccount.slice(-4)})` + } else if (record.payType === 30 && record.bankCard) { + return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})` + } + return '未知账户' + } + + // 刷新数据 + const handleRefresh = async () => { + setRefreshing(true) + await Promise.all([fetchBalance(), fetchWithdrawRecords()]) + setRefreshing(false) + } + + // 初始化加载数据 + useEffect(() => { + if (dealerUser?.userId) { + fetchBalance().then() + fetchWithdrawRecords().then() } - ] + }, [fetchBalance, fetchWithdrawRecords]) - const getStatusText = (status: string) => { + const getStatusText = (status?: number) => { switch (status) { - case 'completed': return '已到账' - case 'pending': return '处理中' - case 'rejected': return '已拒绝' + case 40: return '已到账' + case 20: return '审核通过' + case 10: return '待审核' + case 30: return '已驳回' default: return '未知' } } - const getStatusColor = (status: string) => { + const getStatusColor = (status?: number) => { switch (status) { - case 'completed': return 'success' - case 'pending': return 'warning' - case 'rejected': return 'danger' + case 40: return 'success' + case 20: return 'success' + case 10: return 'warning' + case 30: return 'danger' default: return 'default' } } - const handleSubmit = (values: any) => { - console.log('提现申请:', values) - Taro.showToast({ - title: '提现申请已提交', - icon: 'success' - }) + const handleSubmit = async (values: any) => { + if (!dealerUser?.userId) { + Taro.showToast({ + title: '用户信息获取失败', + icon: 'error' + }) + return + } + + // 验证提现金额 + const amount = parseFloat(values.amount) + const available = parseFloat(availableAmount.replace(',', '')) + + if (amount < 100) { + Taro.showToast({ + title: '最低提现金额为100元', + icon: 'error' + }) + return + } + + if (amount > available) { + Taro.showToast({ + title: '提现金额超过可用余额', + icon: 'error' + }) + return + } + + try { + setSubmitting(true) + + const withdrawData: ShopDealerWithdraw = { + userId: dealerUser.userId, + money: values.amount, + payType: values.accountType === 'wechat' ? 10 : + values.accountType === 'alipay' ? 20 : 30, + applyStatus: 10, // 待审核 + platform: 'MiniProgram' + } + + // 根据提现方式设置账户信息 + if (values.accountType === 'alipay') { + withdrawData.alipayAccount = values.account + withdrawData.alipayName = values.accountName + } else if (values.accountType === 'bank') { + withdrawData.bankCard = values.account + withdrawData.bankAccount = values.accountName + withdrawData.bankName = values.bankName || '银行卡' + } + + await addShopDealerWithdraw(withdrawData) + + Taro.showToast({ + title: '提现申请已提交', + icon: 'success' + }) + + // 重置表单 + formRef.current?.resetFields() + setSelectedAccount('') + + // 刷新数据 + await handleRefresh() + + // 切换到提现记录页面 + setActiveTab('1') + + } catch (error: any) { + console.error('提现申请失败:', error) + Taro.showToast({ + title: error.message || '提现申请失败', + icon: 'error' + }) + } finally { + setSubmitting(false) + } } const quickAmounts = ['100', '300', '500', '1000'] @@ -165,22 +298,49 @@ const DealerWithdraw: React.FC = () => { - - - + {selectedAccount === 'alipay' && ( + <> + + + + + + + + )} - - - + {selectedAccount === 'bank' && ( + <> + + + + + + + + + + + )} - - - + {selectedAccount === 'wechat' && ( + + + 微信钱包提现将直接转入您的微信零钱 + + + )} - @@ -188,40 +348,64 @@ const DealerWithdraw: React.FC = () => { ) const renderWithdrawRecords = () => ( - - {withdrawRecords.length > 0 ? ( - withdrawRecords.map(record => ( - - - - - 提现金额:¥{record.amount} - - - 提现账户:{record.account} - + + + {loading ? ( + + + 加载中... + + ) : withdrawRecords.length > 0 ? ( + withdrawRecords.map(record => ( + + + + + 提现金额:¥{record.money} + + + 提现账户:{record.accountDisplay} + + + + {getStatusText(record.applyStatus)} + - - {getStatusText(record.status)} - - - - 申请时间:{record.createTime} - {record.completeTime && ( - - 完成时间:{record.completeTime} - - )} + + 申请时间:{record.createTime} + {record.auditTime && ( + + 审核时间:{new Date(record.auditTime).toLocaleString()} + + )} + {record.rejectReason && ( + + 驳回原因:{record.rejectReason} + + )} + - - )) - ) : ( - - )} - + )) + ) : ( + + )} + + ) + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + return ( setActiveTab}> diff --git a/src/pages/index/Header.tsx b/src/pages/index/Header.tsx index 0895f8d..0f52e81 100644 --- a/src/pages/index/Header.tsx +++ b/src/pages/index/Header.tsx @@ -8,6 +8,7 @@ import {TenantId} from "@/config/app"; import {getOrganization} from "@/api/system/organization"; import {myUserVerify} from "@/api/system/userVerify"; import { useShopInfo } from '@/hooks/useShopInfo'; +import {handleInviteRelation} from "@/utils/invite"; import MySearch from "./MySearch"; import './Header.scss'; @@ -105,7 +106,7 @@ const Header = (props: any) => { 'content-type': 'application/json', TenantId }, - success: function (res) { + success: async function (res) { if (res.data.code == 1) { Taro.showToast({ title: res.data.message, @@ -118,6 +119,23 @@ const Header = (props: any) => { Taro.setStorageSync('access_token', res.data.data.access_token) Taro.setStorageSync('UserId', res.data.data.user.userId) setIsLogin(true) + + // 处理邀请关系 + if (res.data.data.user?.userId) { + try { + const inviteSuccess = await handleInviteRelation(res.data.data.user.userId) + if (inviteSuccess) { + Taro.showToast({ + title: '邀请关系建立成功', + icon: 'success', + duration: 2000 + }) + } + } catch (error) { + console.error('处理邀请关系失败:', error) + } + } + // 重新加载小程序 Taro.reLaunch({ url: '/pages/index/index' diff --git a/src/pages/index/HeaderWithHook.tsx b/src/pages/index/HeaderWithHook.tsx index d90155f..da16113 100644 --- a/src/pages/index/HeaderWithHook.tsx +++ b/src/pages/index/HeaderWithHook.tsx @@ -10,22 +10,23 @@ import {myUserVerify} from "@/api/system/userVerify"; import {User} from "@/api/system/user/model"; import { useShopInfo } from '@/hooks/useShopInfo'; import { useUser } from '@/hooks/useUser'; +import {handleInviteRelation} from "@/utils/invite"; import MySearch from "./MySearch"; import './Header.scss'; const Header = (props: any) => { // 使用新的hooks - const { - shopInfo, - loading: shopLoading, - getWebsiteName, - getWebsiteLogo + const { + shopInfo, + loading: shopLoading, + getWebsiteName, + getWebsiteLogo } = useShopInfo(); - - const { - user, - isLoggedIn, - loading: userLoading + + const { + user, + isLoggedIn, + loading: userLoading } = useUser(); const [showBasic, setShowBasic] = useState(false) @@ -37,10 +38,10 @@ const Header = (props: any) => { setStatusBarHeight(res.statusBarHeight) }, }) - + // 注意:商店信息现在通过useShopInfo自动管理,不需要手动获取 // 用户信息现在通过useUser自动管理,不需要手动获取 - + // 如果需要获取openId,可以在用户登录后处理 if (user && !user.openid) { Taro.login({ @@ -88,7 +89,7 @@ const Header = (props: any) => { sceneType: 'save_referee', tenantId: TenantId }, - success: function (res) { + success: async function (res) { if (res.data.code == 1) { Taro.showToast({ title: res.data.message, @@ -100,7 +101,23 @@ const Header = (props: any) => { // 登录成功 Taro.setStorageSync('access_token', res.data.data.access_token) Taro.setStorageSync('UserId', res.data.data.user.userId) - + + // 处理邀请关系 + if (res.data.data.user?.userId) { + try { + const inviteSuccess = await handleInviteRelation(res.data.data.user.userId) + if (inviteSuccess) { + Taro.showToast({ + title: '邀请关系建立成功', + icon: 'success', + duration: 2000 + }) + } + } catch (error) { + console.error('处理邀请关系失败:', error) + } + } + // 重新加载小程序 Taro.reLaunch({ url: '/pages/index/index' @@ -179,7 +196,7 @@ const Header = (props: any) => {

商店信息

网站名称: {getWebsiteName()}
Logo: logo
- +

用户信息

登录状态: {isLoggedIn ? '已登录' : '未登录'}
{user && ( @@ -189,8 +206,8 @@ const Header = (props: any) => {
昵称: {user.nickname}
)} - -