Files
qihuaiflow/frontend/src/pages/admin/Promotion.tsx
T

521 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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;