Phase4剩余: 推广+成就系统前后端完成 (promotion + achievement)
- 新增: Achievement.tsx 成就系统前端页面 (644行) - 新增: achievement_admin.py 成就系统后端API (9个端点) - 新增: migration SQL (achievements, user_achievements, achievement_progress_log) - 更新: api/index.ts 添加推广+成就API方法 (23个方法) - 更新: App.tsx 添加 /admin/promotion 和 /admin/achievement 路由 - 更新: AdminLayout.tsx 添加成就系统导航项和图标 - 修复: Promotion.tsx 导入路径 (@/utils/api → @/api) - 前端构建: 472 modules, 11.23s, 成功 - API测试: 全部6个端点返回 code=0
This commit is contained in:
1 parent
7daeae6fc7
commit
0886c26157
8 files changed
+2674
-447
No files matched your search
@@ -0,0 +1,887 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Phase 4 剩余 - 推广系统管理页面
|
||||
* 推广活动、分销链接、返佣记录、推广统计、排行榜
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { adminAPI } from '@/api';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
interface Promotion {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
reward_type: string;
|
||||
reward_value: number;
|
||||
min_payout: number;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
status: string;
|
||||
max_referrals: number;
|
||||
coupon_id: string;
|
||||
created_by: number;
|
||||
creator_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
link_count: number;
|
||||
total_rewards: number;
|
||||
}
|
||||
|
||||
interface ReferralLink {
|
||||
id: number;
|
||||
code: string;
|
||||
user_id: number;
|
||||
username: string;
|
||||
promotion_id: number;
|
||||
promotion_title: string;
|
||||
target_url: string;
|
||||
channel: string;
|
||||
qr_code_url: string;
|
||||
total_clicks: number;
|
||||
total_registrations: number;
|
||||
total_conversions: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ReferralReward {
|
||||
id: number;
|
||||
referral_code: string;
|
||||
referrer_name: string;
|
||||
referee_name: string;
|
||||
order_id: string;
|
||||
reward_type: string;
|
||||
reward_amount: number;
|
||||
reward_points: number;
|
||||
status: string;
|
||||
payout_method: string;
|
||||
paid_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface PromotionStats {
|
||||
total_promotions: number;
|
||||
active_promotions: number;
|
||||
total_referral_links: number;
|
||||
active_referral_links: number;
|
||||
rewards_by_status: Record<string, { count: number; total: number }>;
|
||||
total_clicks: number;
|
||||
total_registrations: number;
|
||||
total_conversions: number;
|
||||
total_revenue: number;
|
||||
total_rewards_paid: number;
|
||||
}
|
||||
|
||||
// ==================== 主组件 ====================
|
||||
const Promotion: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 数据
|
||||
const [stats, setStats] = useState<PromotionStats | null>(null);
|
||||
const [promotions, setPromotions] = useState<Promotion[]>([]);
|
||||
const [referralLinks, setReferralLinks] = useState<ReferralLink[]>([]);
|
||||
const [rewards, setRewards] = useState<ReferralReward[]>([]);
|
||||
const [leaderboard, setLeaderboard] = useState<any[]>([]);
|
||||
|
||||
// 分页
|
||||
const [promoPage, setPromoPage] = useState(1);
|
||||
const [linkPage, setLinkPage] = useState(1);
|
||||
const [rewardPage, setRewardPage] = useState(1);
|
||||
const [totalPromos, setTotalPromos] = useState(0);
|
||||
const [totalLinks, setTotalLinks] = useState(0);
|
||||
const [totalRewards, setTotalRewards] = useState(0);
|
||||
|
||||
// 弹窗
|
||||
const [showPromoModal, setShowPromoModal] = useState(false);
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [editingPromo, setEditingPromo] = useState<Promotion | null>(null);
|
||||
|
||||
// 加载概览数据
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await adminAPI.getPromotionStats();
|
||||
if (res.success) {
|
||||
setStats(res.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load stats:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载推广活动
|
||||
const loadPromotions = useCallback(async (page: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await adminAPI.getPromotions({ page, page_size: 20 });
|
||||
if (res.success) {
|
||||
setPromotions(res.data.items);
|
||||
setTotalPromos(res.data.total);
|
||||
setPromoPage(page);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载分销链接
|
||||
const loadLinks = useCallback(async (page: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await adminAPI.getReferralLinks({ page, page_size: 20 });
|
||||
if (res.success) {
|
||||
setReferralLinks(res.data.items);
|
||||
setTotalLinks(res.data.total);
|
||||
setLinkPage(page);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载返佣记录
|
||||
const loadRewards = useCallback(async (page: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await adminAPI.getReferralRewards({ page, page_size: 20 });
|
||||
if (res.success) {
|
||||
setRewards(res.data.items);
|
||||
setTotalRewards(res.data.total);
|
||||
setRewardPage(page);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 加载排行榜
|
||||
const loadLeaderboard = useCallback(async () => {
|
||||
try {
|
||||
const res = await adminAPI.getPromotionLeaderboard({ limit: 50 });
|
||||
if (res.success) {
|
||||
setLeaderboard(res.data.leaderboard);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load leaderboard:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, [loadStats]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'promotions') {
|
||||
loadPromotions(1);
|
||||
} else if (activeTab === 'links') {
|
||||
loadLinks(1);
|
||||
} else if (activeTab === 'rewards') {
|
||||
loadRewards(1);
|
||||
} else if (activeTab === 'leaderboard') {
|
||||
loadLeaderboard();
|
||||
}
|
||||
}, [activeTab, loadPromotions, loadLinks, loadRewards, loadLeaderboard]);
|
||||
|
||||
// 创建/更新推广活动
|
||||
const handleSavePromo = async (formData: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (editingPromo) {
|
||||
await adminAPI.updatePromotion(editingPromo.id, formData);
|
||||
} else {
|
||||
await adminAPI.createPromotion(formData);
|
||||
}
|
||||
setShowPromoModal(false);
|
||||
setEditingPromo(null);
|
||||
loadPromotions(promoPage);
|
||||
loadStats();
|
||||
} catch (err: any) {
|
||||
setError(err.message || '保存失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换活动状态
|
||||
const handleTogglePromo = async (id: number) => {
|
||||
try {
|
||||
await adminAPI.togglePromotion(id);
|
||||
loadPromotions(promoPage);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除活动
|
||||
const handleDeletePromo = async (id: number) => {
|
||||
if (!window.confirm('确定删除此推广活动?')) return;
|
||||
try {
|
||||
await adminAPI.deletePromotion(id);
|
||||
loadPromotions(promoPage);
|
||||
loadStats();
|
||||
} catch (err: any) {
|
||||
setError(err.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 生成分销链接
|
||||
const handleGenerateLink = async (formData: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await adminAPI.generateReferralLink(formData);
|
||||
setShowLinkModal(false);
|
||||
loadLinks(linkPage);
|
||||
loadStats();
|
||||
} catch (err: any) {
|
||||
setError(err.message || '生成失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 审核返佣
|
||||
const handleApproveReward = async (id: number) => {
|
||||
try {
|
||||
await adminAPI.approveReward(id);
|
||||
loadRewards(rewardPage);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 标记已打款
|
||||
const handleMarkPaid = async (id: number) => {
|
||||
const payout_method = prompt('请输入提现方式(wechat/alipay/bank/balance):');
|
||||
const payout_account = prompt('请输入提现账号:');
|
||||
if (!payout_method || !payout_account) return;
|
||||
|
||||
try {
|
||||
await adminAPI.markRewardPaid(id, {
|
||||
payout_method,
|
||||
payout_account,
|
||||
payout_transaction_id: `TXN${Date.now()}`
|
||||
});
|
||||
loadRewards(rewardPage);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 渲染 ====================
|
||||
const renderOverview = () => {
|
||||
if (!stats) return <div className="loading">加载中...</div>;
|
||||
|
||||
// 计算返佣状态分布条的宽度
|
||||
const totalRewardsCount = Object.values(stats.rewards_by_status || {}).reduce((a: number, b: any) => a + (b.count || 0), 0) || 1;
|
||||
const statusBars = Object.entries(stats.rewards_by_status || {}).map(([status, data]: [string, any]) => ({
|
||||
status,
|
||||
data,
|
||||
width: Math.min(100, ((data.count || 0) / totalRewardsCount) * 100)
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="overview-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📢</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{stats.total_promotions}</div>
|
||||
<div className="stat-label">推广活动总数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card active">
|
||||
<div className="stat-icon">✅</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{stats.active_promotions}</div>
|
||||
<div className="stat-label">进行中的活动</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">🔗</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{stats.total_referral_links}</div>
|
||||
<div className="stat-label">分销链接总数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">👥</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">{stats.total_registrations || 0}</div>
|
||||
<div className="stat-label">总注册数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">💰</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">¥{(stats.total_revenue || 0).toFixed(2)}</div>
|
||||
<div className="stat-label">产生收入</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">💸</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-value">¥{(stats.total_rewards_paid || 0).toFixed(2)}</div>
|
||||
<div className="stat-label">已发返佣</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 返佣状态分布 */}
|
||||
{statusBars.length > 0 && (
|
||||
<div className="chart-card full-width">
|
||||
<h3>返佣状态分布</h3>
|
||||
<div className="status-bars">
|
||||
{statusBars.map(bar => (
|
||||
<div key={bar.status} className="status-bar-item">
|
||||
<span className="status-label">
|
||||
{bar.status === 'pending' ? '待审核' : bar.status === 'approved' ? '已通过' : bar.status === 'paid' ? '已打款' : '已取消'}
|
||||
</span>
|
||||
<div className="status-bar">
|
||||
<div className={`status-bar-fill ${bar.status}`} style={{ width: `${bar.width}%` }}></div>
|
||||
</div>
|
||||
<span className="status-count">{bar.data.count} 笔 (¥{bar.data.total.toFixed(2)})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromotions = () => (
|
||||
<div className="table-container">
|
||||
<div className="table-header">
|
||||
<h3>推广活动列表</h3>
|
||||
<button className="btn-primary" onClick={() => { setEditingPromo(null); setShowPromoModal(true); }}>
|
||||
+ 创建活动
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>活动标题</th>
|
||||
<th>返佣类型</th>
|
||||
<th>返佣值</th>
|
||||
<th>状态</th>
|
||||
<th>链接数</th>
|
||||
<th>已发返佣</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{promotions.map(promo => (
|
||||
<tr key={promo.id}>
|
||||
<td>{promo.id}</td>
|
||||
<td>
|
||||
<div className="name-cell">
|
||||
<div className="name">{promo.title}</div>
|
||||
{promo.description && <div className="description">{promo.description}</div>}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${promo.reward_type}`}>
|
||||
{promo.reward_type === 'percentage' ? '百分比' : promo.reward_type === 'fixed' ? '固定金额' : '积分'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{promo.reward_type === 'percentage' ? `${promo.reward_value}%` :
|
||||
promo.reward_type === 'fixed' ? `¥${promo.reward_value.toFixed(2)}` :
|
||||
`${promo.reward_value} 分`}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status-badge ${promo.status}`}>
|
||||
{promo.status === 'active' ? '进行中' : promo.status === 'inactive' ? '已停用' : '已过期'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{promo.link_count || 0}</td>
|
||||
<td>¥{(promo.total_rewards || 0).toFixed(2)}</td>
|
||||
<td>{new Date(promo.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<div className="action-buttons">
|
||||
<button className="btn-sm" onClick={() => { setEditingPromo(promo); setShowPromoModal(true); }}>编辑</button>
|
||||
<button className="btn-sm warning" onClick={() => handleTogglePromo(promo.id)}>
|
||||
{promo.status === 'active' ? '停用' : '启用'}
|
||||
</button>
|
||||
<button className="btn-sm danger" onClick={() => handleDeletePromo(promo.id)}>删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className="pagination">
|
||||
<button disabled={promoPage <= 1} onClick={() => loadPromotions(promoPage - 1)}>上一页</button>
|
||||
<span>第 {promoPage} 页 / 共 {Math.ceil(totalPromos / 20)} 页</span>
|
||||
<button disabled={promoPage >= Math.ceil(totalPromos / 20)} onClick={() => loadPromotions(promoPage + 1)}>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLinks = () => (
|
||||
<div className="table-container">
|
||||
<div className="table-header">
|
||||
<h3>分销链接列表</h3>
|
||||
<button className="btn-primary" onClick={() => setShowLinkModal(true)}>
|
||||
+ 生成链接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>推广短码</th>
|
||||
<th>推广人</th>
|
||||
<th>关联活动</th>
|
||||
<th>点击数</th>
|
||||
<th>注册数</th>
|
||||
<th>转化数</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{referralLinks.map(link => (
|
||||
<tr key={link.id}>
|
||||
<td>{link.id}</td>
|
||||
<td>
|
||||
<code className="code">{link.code}</code>
|
||||
<button className="btn-icon" onClick={() => { navigator.clipboard.writeText(link.code); alert('已复制'); }}>📋</button>
|
||||
</td>
|
||||
<td>{link.username}</td>
|
||||
<td>{link.promotion_title || '-'}</td>
|
||||
<td>{link.total_clicks || 0}</td>
|
||||
<td>{link.total_registrations || 0}</td>
|
||||
<td>{link.total_conversions || 0}</td>
|
||||
<td>
|
||||
<span className={`status-badge ${link.status}`}>
|
||||
{link.status === 'active' ? '活跃' : '停用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{new Date(link.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<div className="action-buttons">
|
||||
<button className="btn-sm" onClick={() => {
|
||||
const url = `${link.target_url}?ref=${link.code}`;
|
||||
navigator.clipboard.writeText(url);
|
||||
alert('链接已复制');
|
||||
}}>复制链接</button>
|
||||
<button className="btn-sm warning" onClick={async () => {
|
||||
if (window.confirm('确定重新生成推广短码?')) {
|
||||
try {
|
||||
await adminAPI.regenerateReferralCode(link.id);
|
||||
loadLinks(linkPage);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '操作失败');
|
||||
}
|
||||
}
|
||||
}}>重新生成</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRewards = () => (
|
||||
<div className="table-container">
|
||||
<div className="table-header">
|
||||
<h3>返佣记录</h3>
|
||||
</div>
|
||||
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>推广码</th>
|
||||
<th>推广人</th>
|
||||
<th>被推荐人</th>
|
||||
<th>订单号</th>
|
||||
<th>返佣类型</th>
|
||||
<th>返佣金额</th>
|
||||
<th>状态</th>
|
||||
<th>打款时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rewards.map(reward => (
|
||||
<tr key={reward.id}>
|
||||
<td>{reward.id}</td>
|
||||
<td><code className="code">{reward.referral_code}</code></td>
|
||||
<td>{reward.referrer_name}</td>
|
||||
<td>{reward.referee_name}</td>
|
||||
<td>{reward.order_id || '-'}</td>
|
||||
<td>
|
||||
<span className="badge">
|
||||
{reward.reward_type === 'registration' ? '注册奖励' :
|
||||
reward.reward_type === 'first_purchase' ? '首购返佣' : '续费返佣'}
|
||||
</span>
|
||||
</td>
|
||||
<td>¥{reward.reward_amount.toFixed(2)}</td>
|
||||
<td>
|
||||
<span className={`status-badge ${reward.status}`}>
|
||||
{reward.status === 'pending' ? '待审核' :
|
||||
reward.status === 'approved' ? '已通过' :
|
||||
reward.status === 'paid' ? '已打款' : '已取消'}
|
||||
</span>
|
||||
</td>
|
||||
<td>{reward.paid_at ? new Date(reward.paid_at).toLocaleDateString() : '-'}</td>
|
||||
<td>
|
||||
<div className="action-buttons">
|
||||
{reward.status === 'pending' && (
|
||||
<button className="btn-sm" onClick={() => handleApproveReward(reward.id)}>审核通过</button>
|
||||
)}
|
||||
{reward.status === 'approved' && (
|
||||
<button className="btn-sm warning" onClick={() => handleMarkPaid(reward.id)}>标记已打款</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLeaderboard = () => (
|
||||
<div className="table-container">
|
||||
<div className="table-header">
|
||||
<h3>推广排行榜</h3>
|
||||
</div>
|
||||
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>用户</th>
|
||||
<th>点击数</th>
|
||||
<th>注册数</th>
|
||||
<th>转化数</th>
|
||||
<th>产生收入</th>
|
||||
<th>获得返佣</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{leaderboard.map((entry, index) => (
|
||||
<tr key={entry.user_id}>
|
||||
<td>
|
||||
<span className={`rank ${index < 3 ? 'top' : ''}`}>
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : entry.rank}
|
||||
</span>
|
||||
</td>
|
||||
<td>{entry.username}</td>
|
||||
<td>{entry.total_clicks || 0}</td>
|
||||
<td>{entry.total_registrations || 0}</td>
|
||||
<td>{entry.total_conversions || 0}</td>
|
||||
<td>¥{(entry.total_revenue || 0).toFixed(2)}</td>
|
||||
<td>¥{(entry.total_rewards || 0).toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="promotion-page">
|
||||
<div className="page-header">
|
||||
<h2>📢 推广系统管理</h2>
|
||||
<p>管理推广活动、分销链接、返佣记录和推广统计</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
|
||||
<div className="tabs">
|
||||
<button className={`tab ${activeTab === 'overview' ? 'active' : ''}`} onClick={() => setActiveTab('overview')}>
|
||||
📊 概览
|
||||
</button>
|
||||
<button className={`tab ${activeTab === 'promotions' ? 'active' : ''}`} onClick={() => setActiveTab('promotions')}>
|
||||
📢 推广活动
|
||||
</button>
|
||||
<button className={`tab ${activeTab === 'links' ? 'active' : ''}`} onClick={() => setActiveTab('links')}>
|
||||
🔗 分销链接
|
||||
</button>
|
||||
<button className={`tab ${activeTab === 'rewards' ? 'active' : ''}`} onClick={() => setActiveTab('rewards')}>
|
||||
💰 返佣记录
|
||||
</button>
|
||||
<button className={`tab ${activeTab === 'leaderboard' ? 'active' : ''}`} onClick={() => setActiveTab('leaderboard')}>
|
||||
🏆 排行榜
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tab-content">
|
||||
{activeTab === 'overview' && renderOverview()}
|
||||
{activeTab === 'promotions' && renderPromotions()}
|
||||
{activeTab === 'links' && renderLinks()}
|
||||
{activeTab === 'rewards' && renderRewards()}
|
||||
{activeTab === 'leaderboard' && renderLeaderboard()}
|
||||
</div>
|
||||
|
||||
{/* 推广活动弹窗 */}
|
||||
{showPromoModal && (
|
||||
<PromotionModal
|
||||
promotion={editingPromo}
|
||||
onSave={handleSavePromo}
|
||||
onCancel={() => { setShowPromoModal(false); setEditingPromo(null); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 生成链接弹窗 */}
|
||||
{showLinkModal && (
|
||||
<LinkModal
|
||||
onSave={handleGenerateLink}
|
||||
onCancel={() => setShowLinkModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 推广活动弹窗 ====================
|
||||
interface PromotionModalProps {
|
||||
promotion: Promotion | null;
|
||||
onSave: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const PromotionModal: React.FC<PromotionModalProps> = ({ promotion, onSave, onCancel }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
title: promotion?.title || '',
|
||||
description: promotion?.description || '',
|
||||
reward_type: promotion?.reward_type || 'percentage',
|
||||
reward_value: promotion?.reward_value || 5,
|
||||
min_payout: promotion?.min_payout || 10,
|
||||
start_date: promotion?.start_date ? promotion.start_date.slice(0, 16) : '',
|
||||
end_date: promotion?.end_date ? promotion.end_date.slice(0, 16) : '',
|
||||
max_referrals: promotion?.max_referrals || 0,
|
||||
status: promotion?.status || 'active'
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h3>{promotion ? '编辑推广活动' : '创建推广活动'}</h3>
|
||||
<button className="close-btn" onClick={onCancel}>×</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="form-group">
|
||||
<label>活动标题 *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.title}
|
||||
onChange={e => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="例如:新用户推广计划"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>活动描述</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={e => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="活动说明..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>返佣类型 *</label>
|
||||
<select
|
||||
value={formData.reward_type}
|
||||
onChange={e => setFormData({ ...formData, reward_type: e.target.value })}
|
||||
>
|
||||
<option value="percentage">百分比</option>
|
||||
<option value="fixed">固定金额</option>
|
||||
<option value="points">积分</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>返佣值 *</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
step={formData.reward_type === 'percentage' ? 1 : 0.01}
|
||||
min={0}
|
||||
value={formData.reward_value}
|
||||
onChange={e => setFormData({ ...formData, reward_value: parseFloat(e.target.value) })}
|
||||
placeholder={formData.reward_type === 'percentage' ? '5 表示 5%' : '100 表示 100元'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>最低提现金额</label>
|
||||
<input
|
||||
type="number"
|
||||
step={0.01}
|
||||
min={0}
|
||||
value={formData.min_payout}
|
||||
onChange={e => setFormData({ ...formData, min_payout: parseFloat(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>最大推广人数 (0=无限制)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={formData.max_referrals}
|
||||
onChange={e => setFormData({ ...formData, max_referrals: parseInt(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>开始时间 *</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={formData.start_date}
|
||||
onChange={e => setFormData({ ...formData, start_date: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>结束时间 (可选)</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={formData.end_date}
|
||||
onChange={e => setFormData({ ...formData, end_date: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn-secondary" onClick={onCancel}>取消</button>
|
||||
<button type="submit" className="btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 生成链接弹窗 ====================
|
||||
interface LinkModalProps {
|
||||
onSave: (data: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const LinkModal: React.FC<LinkModalProps> = ({ onSave, onCancel }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
promotion_id: '',
|
||||
target_url: 'https://bot.qihuyun.com/register',
|
||||
channel: 'link'
|
||||
});
|
||||
const [promotions, setPromotions] = useState<Promotion[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
adminAPI.getPromotions({ status: 'active', page: 1, page_size: 100 })
|
||||
.then(res => {
|
||||
if (res.success) setPromotions(res.data.items);
|
||||
})
|
||||
.catch(err => console.error('Failed to load active promotions:', err));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave({
|
||||
promotion_id: parseInt(formData.promotion_id),
|
||||
target_url: formData.target_url,
|
||||
channel: formData.channel
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h3>生成分销链接</h3>
|
||||
<button className="close-btn" onClick={onCancel}>×</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="form-group">
|
||||
<label>选择推广活动 *</label>
|
||||
<select
|
||||
required
|
||||
value={formData.promotion_id}
|
||||
onChange={e => setFormData({ ...formData, promotion_id: e.target.value })}
|
||||
>
|
||||
<option value="">-- 请选择 --</option>
|
||||
{promotions.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>目标URL *</label>
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
value={formData.target_url}
|
||||
onChange={e => setFormData({ ...formData, target_url: e.target.value })}
|
||||
placeholder="https://bot.qihuyun.com/register"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>推广渠道</label>
|
||||
<select
|
||||
value={formData.channel}
|
||||
onChange={e => setFormData({ ...formData, channel: e.target.value })}
|
||||
>
|
||||
<option value="link">链接</option>
|
||||
<option value="qrcode">二维码</option>
|
||||
<option value="api">API</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn-secondary" onClick={onCancel}>取消</button>
|
||||
<button type="submit" className="btn-primary">生成链接</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Promotion;
|
||||
Reference in new issue
Block a user