521 lines
31 KiB
TypeScript
521 lines
31 KiB
TypeScript
// @ts-nocheck
|
||
/**
|
||
* 推广系统管理页面 — 使用 admin 设计系统
|
||
*/
|
||
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { adminAPI } from '@/api';
|
||
|
||
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
|
||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
{children}
|
||
</svg>
|
||
);
|
||
|
||
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;
|
||
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; 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;
|
||
}
|
||
|
||
const Promotion: React.FC = () => {
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'promotions' | 'links' | 'rewards' | 'leaderboard'>('overview');
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
const [stats, setStats] = useState<any>(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 [totalPromos, setTotalPromos] = 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();
|
||
const data = (res as any)?.data || res || {};
|
||
setStats(data);
|
||
} catch (err) { console.error('Stats load failed:', err); }
|
||
}, []);
|
||
|
||
const loadPromotions = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await adminAPI.getPromotions({ page: promoPage, page_size: 20 });
|
||
const data = (res as any)?.data || res || {};
|
||
setPromotions(data.items || []);
|
||
setTotalPromos(data.total || 0);
|
||
} catch (err) { setError('加载失败'); }
|
||
finally { setLoading(false); }
|
||
}, [promoPage]);
|
||
|
||
const loadLinks = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await adminAPI.getReferralLinks({ page: 1, page_size: 50 });
|
||
const data = (res as any)?.data || res || {};
|
||
setReferralLinks(data.items || []);
|
||
} catch (err) { setError('加载失败'); }
|
||
finally { setLoading(false); }
|
||
}, []);
|
||
|
||
const loadRewards = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await adminAPI.getReferralRewards({ page: 1, page_size: 50 });
|
||
const data = (res as any)?.data || res || {};
|
||
setRewards(data.items || []);
|
||
} catch (err) { setError('加载失败'); }
|
||
finally { setLoading(false); }
|
||
}, []);
|
||
|
||
const loadLeaderboard = useCallback(async () => {
|
||
try {
|
||
const res = await adminAPI.getPromotionLeaderboard({ limit: 50 });
|
||
const data = (res as any)?.data || res || {};
|
||
setLeaderboard(data.items || data.leaderboard || []);
|
||
} catch (err) { console.error('Leaderboard load failed:', err); }
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
setError('');
|
||
if (activeTab === 'overview') loadStats();
|
||
else if (activeTab === 'promotions') loadPromotions();
|
||
else if (activeTab === 'links') loadLinks();
|
||
else if (activeTab === 'rewards') loadRewards();
|
||
else if (activeTab === 'leaderboard') loadLeaderboard();
|
||
}, [activeTab, loadStats, 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(); loadStats();
|
||
} catch (err) { setError('保存失败'); }
|
||
finally { setLoading(false); }
|
||
};
|
||
|
||
const handleTogglePromo = async (id: number) => {
|
||
try { await adminAPI.togglePromotion(id); loadPromotions(); }
|
||
catch (err) { setError('操作失败'); }
|
||
};
|
||
|
||
const handleDeletePromo = async (id: number) => {
|
||
if (!window.confirm('确定删除此推广活动?')) return;
|
||
try { await adminAPI.deletePromotion(id); loadPromotions(); loadStats(); }
|
||
catch (err) { setError('删除失败'); }
|
||
};
|
||
|
||
const handleGenerateLink = async (formData: any) => {
|
||
setLoading(true);
|
||
try { await adminAPI.generateReferralLink(formData); setShowLinkModal(false); loadLinks(); loadStats(); }
|
||
catch (err) { setError('生成失败'); }
|
||
finally { setLoading(false); }
|
||
};
|
||
|
||
const handleApproveReward = async (id: number) => {
|
||
try { await adminAPI.approveReward(id); loadRewards(); }
|
||
catch (err) { setError('操作失败'); }
|
||
};
|
||
|
||
const handleMarkPaid = async (id: number) => {
|
||
const payout_method = prompt('请输入提现方式(wechat/alipay/bank/balance):');
|
||
if (!payout_method) return;
|
||
try {
|
||
await adminAPI.markRewardPaid(id, { payout_method, payout_transaction_id: `TXN${Date.now()}` });
|
||
loadRewards();
|
||
} catch (err) { setError('操作失败'); }
|
||
};
|
||
|
||
const inputStyle: React.CSSProperties = {
|
||
background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)',
|
||
borderRadius: 'var(--radius-md)', padding: '8px 12px', color: 'var(--color-text)',
|
||
fontSize: 'var(--text-sm)', width: '100%', outline: 'none',
|
||
};
|
||
const labelStyle: React.CSSProperties = {
|
||
fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', fontWeight: 600, marginBottom: 4, display: 'block',
|
||
};
|
||
|
||
const tabs = [
|
||
{ key: 'overview', label: '概览', icon: <><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></> },
|
||
{ key: 'promotions', label: '推广活动', icon: <><path d="M3 11l18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></> },
|
||
{ key: 'links', label: '分销链接', icon: <><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></> },
|
||
{ key: 'rewards', label: '返佣记录', icon: <><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></> },
|
||
{ key: 'leaderboard', label: '排行榜', icon: <><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2z"/></> },
|
||
];
|
||
|
||
return (
|
||
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-xl)' }}>
|
||
<div>
|
||
<h1 className="admin-page-title">推广系统管理</h1>
|
||
<p style={{ fontSize: 'var(--text-sm)', color: 'var(--color-text-tertiary)', marginTop: 4 }}>管理推广活动、分销链接、返佣记录和推广统计</p>
|
||
</div>
|
||
{activeTab === 'promotions' && (
|
||
<button className="admin-btn admin-btn-primary" onClick={() => { setEditingPromo(null); setShowPromoModal(true); }}>
|
||
<SVGIcon size={14}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></SVGIcon>
|
||
创建活动
|
||
</button>
|
||
)}
|
||
{activeTab === 'links' && (
|
||
<button className="admin-btn admin-btn-primary" onClick={() => setShowLinkModal(true)}>
|
||
<SVGIcon size={14}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></SVGIcon>
|
||
生成链接
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{error && <div style={{ padding: '8px 16px', background: 'rgba(239,68,68,0.1)', color: '#ef4444', borderRadius: 'var(--radius-md)', marginBottom: 'var(--space-md)', fontSize: 'var(--text-sm)' }}>{error}</div>}
|
||
|
||
{/* Tabs */}
|
||
<div style={{ display: 'flex', gap: 2, marginBottom: 'var(--space-xl)', borderBottom: '1px solid var(--color-border)' }}>
|
||
{tabs.map(tab => (
|
||
<button key={tab.key} onClick={() => setActiveTab(tab.key as any)}
|
||
style={{
|
||
padding: '10px 20px', border: 'none', cursor: 'pointer',
|
||
background: activeTab === tab.key ? 'var(--color-bg-secondary)' : 'transparent',
|
||
color: activeTab === tab.key ? 'var(--color-accent)' : 'var(--color-text-secondary)',
|
||
fontSize: 'var(--text-sm)', fontWeight: activeTab === tab.key ? 600 : 400,
|
||
borderBottom: activeTab === tab.key ? '2px solid var(--color-accent)' : '2px solid transparent',
|
||
transition: 'all 0.2s', display: 'flex', alignItems: 'center', gap: 6,
|
||
}}>
|
||
<SVGIcon size={14}>{tab.icon}</SVGIcon>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* ===================== Overview Tab ===================== */}
|
||
{activeTab === 'overview' && (
|
||
<div>
|
||
{!stats ? (
|
||
<div style={{ textAlign: 'center', padding: 'var(--space-2xl)', color: 'var(--color-text-tertiary)' }}>加载中...</div>
|
||
) : (
|
||
<>
|
||
<div className="responsive-grid-4" style={{ marginBottom: 'var(--space-xl)' }}>
|
||
{[
|
||
{ label: '推广活动总数', value: stats.total_promotions || 0, color: '#818cf8', icon: 'M3 11l18-5v12L3 14v-3z' },
|
||
{ label: '进行中的活动', value: stats.active_promotions || 0, color: '#22c55e', icon: 'M9 12l2 2 4-4' },
|
||
{ label: '分销链接总数', value: stats.total_referrals || stats.total_referral_links || 0, color: '#f59e0b', icon: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' },
|
||
{ label: '已发返佣总额', value: `¥${(stats.total_rewards || stats.total_rewards_paid || 0).toFixed(2)}`, color: '#06b6d4', icon: 'M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' },
|
||
].map((s, i) => (
|
||
<div key={i} className="admin-card" style={{ marginBottom: 0, textAlign: 'center' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 'var(--space-sm)' }}>
|
||
<div style={{ width: 40, height: 40, borderRadius: 'var(--radius-full)', background: `${s.color}15`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<SVGIcon size={20}><path d={s.icon}/></SVGIcon>
|
||
</div>
|
||
</div>
|
||
<div style={{ fontSize: 'var(--text-2xl)', fontWeight: 800, color: s.color }}>{s.value}</div>
|
||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--color-text-tertiary)', marginTop: 4 }}>{s.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="admin-card">
|
||
<div className="admin-card-header"><h3 className="admin-card-title">待审核返佣</h3></div>
|
||
<div style={{ padding: 'var(--space-lg)', textAlign: 'center' }}>
|
||
<div style={{ fontSize: 'var(--text-3xl)', fontWeight: 800, color: '#f59e0b' }}>¥{(stats.pending_rewards || 0).toFixed(2)}</div>
|
||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--color-text-tertiary)', marginTop: 4 }}>待审核返佣金额</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ===================== Promotions Tab ===================== */}
|
||
{activeTab === 'promotions' && (
|
||
<div className="admin-card" style={{ overflowX: 'auto' }}>
|
||
<table className="admin-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.length === 0 ? (
|
||
<tr><td colSpan={9} style={{ textAlign: 'center', padding: 'var(--space-2xl)', color: 'var(--color-text-tertiary)' }}>暂无数据</td></tr>
|
||
) : promotions.map(promo => (
|
||
<tr key={promo.id}>
|
||
<td>{promo.id}</td>
|
||
<td>
|
||
<div style={{ fontWeight: 600 }}>{promo.title}</div>
|
||
{promo.description && <div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{promo.description}</div>}
|
||
</td>
|
||
<td>
|
||
<span className="admin-badge" style={{ background: 'rgba(99,102,241,0.1)', color: '#818cf8' }}>
|
||
{promo.reward_type === 'percentage' ? '百分比' : promo.reward_type === 'fixed' ? '固定金额' : '积分'}
|
||
</span>
|
||
</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>
|
||
{promo.reward_type === 'percentage' ? `${promo.reward_value}%` :
|
||
promo.reward_type === 'fixed' ? `¥${promo.reward_value?.toFixed(2)}` : `${promo.reward_value} 分`}
|
||
</td>
|
||
<td>
|
||
<span className={`admin-badge ${promo.status === 'active' ? 'badge-success' : 'badge-neutral'}`}>
|
||
{promo.status === 'active' ? '进行中' : promo.status === 'inactive' ? '已停用' : '已过期'}
|
||
</span>
|
||
</td>
|
||
<td>{promo.link_count || 0}</td>
|
||
<td>¥{(promo.total_rewards || 0).toFixed(2)}</td>
|
||
<td style={{ fontSize: 'var(--text-xs)' }}>{promo.created_at ? new Date(promo.created_at).toLocaleDateString('zh-CN') : '-'}</td>
|
||
<td>
|
||
<div style={{ display: 'flex', gap: 4 }}>
|
||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => { setEditingPromo(promo); setShowPromoModal(true); }}>编辑</button>
|
||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleTogglePromo(promo.id)}>
|
||
{promo.status === 'active' ? '停用' : '启用'}
|
||
</button>
|
||
<button className="admin-btn admin-btn-outline admin-btn-sm" style={{ color: '#ef4444' }} onClick={() => handleDeletePromo(promo.id)}>删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
{totalPromos > 20 && (
|
||
<div className="pagination-bar" style={{ marginTop: 'var(--space-md)' }}>
|
||
<button className="admin-btn admin-btn-outline admin-btn-sm" disabled={promoPage <= 1} onClick={() => setPromoPage(p => p - 1)}>上一页</button>
|
||
<span style={{ fontSize: 'var(--text-sm)' }}>第 {promoPage} 页 / 共 {Math.ceil(totalPromos / 20)} 页</span>
|
||
<button className="admin-btn admin-btn-outline admin-btn-sm" disabled={promoPage >= Math.ceil(totalPromos / 20)} onClick={() => setPromoPage(p => p + 1)}>下一页</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ===================== Links Tab ===================== */}
|
||
{activeTab === 'links' && (
|
||
<div className="admin-card" style={{ overflowX: 'auto' }}>
|
||
<table className="admin-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>
|
||
{referralLinks.length === 0 ? (
|
||
<tr><td colSpan={9} style={{ textAlign: 'center', padding: 'var(--space-2xl)', color: 'var(--color-text-tertiary)' }}>暂无数据</td></tr>
|
||
) : referralLinks.map(link => (
|
||
<tr key={link.id}>
|
||
<td>{link.id}</td>
|
||
<td><code style={{ background: 'var(--color-bg-tertiary)', padding: '2px 8px', borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)' }}>{link.code}</code></td>
|
||
<td>{link.username}</td>
|
||
<td>{link.promotion_title || '-'}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>{link.total_clicks || 0}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>{link.total_registrations || 0}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>{link.total_conversions || 0}</td>
|
||
<td>
|
||
<span className={`admin-badge ${link.status === 'active' ? 'badge-success' : 'badge-neutral'}`}>
|
||
{link.status === 'active' ? '活跃' : '停用'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => {
|
||
const url = `${link.target_url}?ref=${link.code}`;
|
||
navigator.clipboard.writeText(url);
|
||
alert('链接已复制');
|
||
}}>复制链接</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* ===================== Rewards Tab ===================== */}
|
||
{activeTab === 'rewards' && (
|
||
<div className="admin-card" style={{ overflowX: 'auto' }}>
|
||
<table className="admin-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>
|
||
{rewards.length === 0 ? (
|
||
<tr><td colSpan={9} style={{ textAlign: 'center', padding: 'var(--space-2xl)', color: 'var(--color-text-tertiary)' }}>暂无数据</td></tr>
|
||
) : rewards.map(reward => (
|
||
<tr key={reward.id}>
|
||
<td>{reward.id}</td>
|
||
<td><code style={{ background: 'var(--color-bg-tertiary)', padding: '2px 8px', borderRadius: 'var(--radius-sm)', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)' }}>{reward.referral_code}</code></td>
|
||
<td>{reward.referrer_name}</td>
|
||
<td>{reward.referee_name}</td>
|
||
<td>
|
||
<span className="admin-badge" style={{ background: 'rgba(99,102,241,0.1)', color: '#818cf8' }}>
|
||
{reward.reward_type === 'registration' ? '注册奖励' : reward.reward_type === 'first_purchase' ? '首购返佣' : '续费返佣'}
|
||
</span>
|
||
</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>¥{reward.reward_amount?.toFixed(2)}</td>
|
||
<td>
|
||
<span className={`admin-badge ${reward.status === 'paid' ? 'badge-success' : reward.status === 'approved' ? 'badge-success' : reward.status === 'pending' ? 'badge-neutral' : 'badge-danger'}`}>
|
||
{reward.status === 'pending' ? '待审核' : reward.status === 'approved' ? '已通过' : reward.status === 'paid' ? '已打款' : '已取消'}
|
||
</span>
|
||
</td>
|
||
<td style={{ fontSize: 'var(--text-xs)' }}>{reward.paid_at ? new Date(reward.paid_at).toLocaleDateString('zh-CN') : '-'}</td>
|
||
<td>
|
||
<div style={{ display: 'flex', gap: 4 }}>
|
||
{reward.status === 'pending' && <button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleApproveReward(reward.id)}>审核通过</button>}
|
||
{reward.status === 'approved' && <button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleMarkPaid(reward.id)}>标记已打款</button>}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* ===================== Leaderboard Tab ===================== */}
|
||
{activeTab === 'leaderboard' && (
|
||
<div className="admin-card" style={{ overflowX: 'auto' }}>
|
||
<table className="admin-table">
|
||
<thead>
|
||
<tr><th>排名</th><th>用户</th><th>点击数</th><th>注册数</th><th>转化数</th><th>产生收入</th><th>获得返佣</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{leaderboard.length === 0 ? (
|
||
<tr><td colSpan={7} style={{ textAlign: 'center', padding: 'var(--space-2xl)', color: 'var(--color-text-tertiary)' }}>暂无数据</td></tr>
|
||
) : leaderboard.map((entry, index) => (
|
||
<tr key={entry.user_id || index}>
|
||
<td>
|
||
<span style={{ fontSize: 'var(--text-lg)' }}>{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `#${index + 1}`}</span>
|
||
</td>
|
||
<td style={{ fontWeight: 600 }}>{entry.username}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>{entry.total_clicks || 0}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>{entry.total_registrations || 0}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>{entry.total_conversions || 0}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>¥{(entry.total_revenue || 0).toFixed(2)}</td>
|
||
<td style={{ fontFamily: 'var(--font-mono)' }}>¥{(entry.total_rewards || 0).toFixed(2)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
|
||
{/* ===================== Promotion Modal ===================== */}
|
||
{showPromoModal && (
|
||
<PromotionModal promotion={editingPromo} onSave={handleSavePromo} onCancel={() => { setShowPromoModal(false); setEditingPromo(null); }} />
|
||
)}
|
||
|
||
{/* ===================== Link Modal ===================== */}
|
||
{showLinkModal && (
|
||
<LinkModal onSave={handleGenerateLink} onCancel={() => setShowLinkModal(false)} />
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ==================== 推广活动弹窗 ====================
|
||
const PromotionModal: React.FC<{ promotion: Promotion | null; onSave: (data: any) => void; onCancel: () => void }> = ({ 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 inputStyle: React.CSSProperties = {
|
||
background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)',
|
||
borderRadius: 'var(--radius-md)', padding: '8px 12px', color: 'var(--color-text)',
|
||
fontSize: 'var(--text-sm)', width: '100%', outline: 'none',
|
||
};
|
||
const labelStyle: React.CSSProperties = {
|
||
fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', fontWeight: 600, marginBottom: 4, display: 'block',
|
||
};
|
||
|
||
return (
|
||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', zIndex: 999, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: '4vh' }} onClick={onCancel}>
|
||
<div style={{ background: 'var(--color-bg-primary)', borderRadius: 'var(--radius-xl)', width: 600, maxHeight: '90vh', overflow: 'auto', boxShadow: 'var(--shadow-xl)' }} onClick={e => e.stopPropagation()}>
|
||
<div style={{ padding: 'var(--space-xl) var(--space-2xl)', borderBottom: '1px solid var(--color-border)' }}>
|
||
<h3 style={{ fontSize: 'var(--text-lg)', fontWeight: 700 }}>{promotion ? '编辑推广活动' : '创建推广活动'}</h3>
|
||
</div>
|
||
<form onSubmit={e => { e.preventDefault(); onSave(formData); }}>
|
||
<div style={{ padding: 'var(--space-xl) var(--space-2xl)', display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' }}>
|
||
<div><label style={labelStyle}>活动标题 *</label><input style={inputStyle} required value={formData.title} onChange={e => setFormData({ ...formData, title: e.target.value })} placeholder="例如:新用户推广计划" /></div>
|
||
<div><label style={labelStyle}>活动描述</label><textarea style={{ ...inputStyle, resize: 'vertical' }} value={formData.description} onChange={e => setFormData({ ...formData, description: e.target.value })} rows={3} placeholder="活动说明..." /></div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-md)' }}>
|
||
<div><label style={labelStyle}>返佣类型 *</label><select style={inputStyle} 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><label style={labelStyle}>返佣值 *</label><input type="number" style={inputStyle} 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) })} /></div>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-md)' }}>
|
||
<div><label style={labelStyle}>最低提现金额</label><input type="number" style={inputStyle} step={0.01} min={0} value={formData.min_payout} onChange={e => setFormData({ ...formData, min_payout: parseFloat(e.target.value) })} /></div>
|
||
<div><label style={labelStyle}>最大推广人数 (0=不限)</label><input type="number" style={inputStyle} min={0} value={formData.max_referrals} onChange={e => setFormData({ ...formData, max_referrals: parseInt(e.target.value) })} /></div>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-md)' }}>
|
||
<div><label style={labelStyle}>开始时间 *</label><input type="datetime-local" style={inputStyle} required value={formData.start_date} onChange={e => setFormData({ ...formData, start_date: e.target.value })} /></div>
|
||
<div><label style={labelStyle}>结束时间 (可选)</label><input type="datetime-local" style={inputStyle} value={formData.end_date} onChange={e => setFormData({ ...formData, end_date: e.target.value })} /></div>
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: 'var(--space-lg) var(--space-2xl)', display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-md)', borderTop: '1px solid var(--color-border)' }}>
|
||
<button type="button" className="admin-btn admin-btn-outline" onClick={onCancel}>取消</button>
|
||
<button type="submit" className="admin-btn admin-btn-primary">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ==================== 生成链接弹窗 ====================
|
||
const LinkModal: React.FC<{ onSave: (data: any) => void; onCancel: () => void }> = ({ 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 => { const d = (res as any)?.data || res; if (d?.items) setPromotions(d.items); })
|
||
.catch(err => console.error('Failed to load promotions:', err));
|
||
}, []);
|
||
|
||
const inputStyle: React.CSSProperties = {
|
||
background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)',
|
||
borderRadius: 'var(--radius-md)', padding: '8px 12px', color: 'var(--color-text)',
|
||
fontSize: 'var(--text-sm)', width: '100%', outline: 'none',
|
||
};
|
||
const labelStyle: React.CSSProperties = {
|
||
fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', fontWeight: 600, marginBottom: 4, display: 'block',
|
||
};
|
||
|
||
return (
|
||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', zIndex: 999, display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={onCancel}>
|
||
<div style={{ background: 'var(--color-bg-primary)', borderRadius: 'var(--radius-xl)', width: 480, padding: 'var(--space-2xl)' }} onClick={e => e.stopPropagation()}>
|
||
<h3 style={{ fontSize: 'var(--text-lg)', fontWeight: 700, marginBottom: 'var(--space-xl)' }}>生成分销链接</h3>
|
||
<form onSubmit={e => { e.preventDefault(); onSave({ promotion_id: parseInt(formData.promotion_id), target_url: formData.target_url, channel: formData.channel }); }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' }}>
|
||
<div><label style={labelStyle}>选择推广活动 *</label><select style={inputStyle} 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><label style={labelStyle}>目标URL *</label><input type="url" style={inputStyle} required value={formData.target_url} onChange={e => setFormData({ ...formData, target_url: e.target.value })} /></div>
|
||
<div><label style={labelStyle}>推广渠道</label><select style={inputStyle} 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 style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-md)', marginTop: 'var(--space-xl)' }}>
|
||
<button type="button" className="admin-btn admin-btn-outline" onClick={onCancel}>取消</button>
|
||
<button type="submit" className="admin-btn admin-btn-primary">生成链接</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Promotion;
|