import React, { useState, useEffect, useCallback } from 'react'; import { adminAPI } from '@/api'; import McpMarketTab from './McpMarketTab'; // ============================================================ // Types // ============================================================ interface MarketAppItem { id: string; name: string; type: string; category: string; tags: string | null; source: string; publisher_name: string | null; description: string | null; icon: string | null; price: number; is_member_free: boolean; featured: boolean; featured_order: number; status: string; rating_avg: number; review_count: number; install_count: number; version: string; created_at: string | null; updated_at: string | null; tutorial?: string; skill_content?: string; } interface OverviewData { total_apps: number; published: number; pending: number; removed: number; total_installs: number; total_purchases: number; total_revenue: number; total_reviews: number; overall_rating: number; by_type: Record; by_category: Record; trend_7d: Array<{ date: string; new_apps: number; new_installs: number }>; } interface CouponItem { id: string; code: string; type: string; value: number; min_amount: number; valid_from: string | null; valid_to: string | null; total_count: number; used_count: number; status: string; } interface ReviewItem { id: string; rating: number; content: string; username: string; avatar: string | null; app_name: string; app_icon: string | null; created_at: string | null; } const CATEGORY_LABELS: Record = { coding: '编程开发', assistant: '智能助手', tools: '效率工具', writing: '内容创作', education: '教育培训', marketing: '营销推广', data: '数据分析', design: '设计创意', business: '企业管理', other: '其他', }; const TYPE_LABELS: Record = { skill: 'Skills工具', agent: '智能Agent', mcp: 'MCP协议', }; const STATUS_LABELS: Record = { pending: '待审核', published: '已发布', removed: '已下架', }; const STATUS_COLORS: Record = { pending: 'var(--color-warning)', published: 'var(--color-success)', removed: 'var(--color-danger)', }; // ============================================================ // Icon helpers // ============================================================ const Icon = ({ name, size = 18, color = 'currentColor' }: { name: string; size?: number; color?: string }) => { const icons: Record = { chart: 'M3 3v18h18M7 14l4-4 4 4 5-5', store: 'M3 9l1-5h16l1 5M4 9v11h16V9M9 14h6', check: 'M20 6L9 17l-5-5', close: 'M18 6L6 18M6 6l12 12', edit: 'M11 4H4v16h16v-7M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z', star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', trash: 'M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2', tag: 'M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z', plus: 'M12 5v14M5 12h14', search: 'M11 4a7 7 0 100 14 7 7 0 000-14zM21 21l-4.35-4.35', download: 'M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3', refresh: 'M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15', trending: 'M23 6l-9.5 9.5-5-5L1 18', eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z', }; return ( ); }; // ============================================================ // Main Component // ============================================================ const Market: React.FC = () => { const [activeTab, setActiveTab] = useState<'overview' | 'apps' | 'pending' | 'coupons' | 'reviews' | 'mcp'>('overview'); const [loading, setLoading] = useState(false); // Overview data const [overview, setOverview] = useState(null); const [topApps, setTopApps] = useState([]); const [topMetric, setTopMetric] = useState('installs'); // Apps list const [apps, setApps] = useState([]); const [appsTotal, setAppsTotal] = useState(0); const [appsPage, setAppsPage] = useState(1); const [filterStatus, setFilterStatus] = useState(''); const [filterType, setFilterType] = useState(''); const [filterCategory, setFilterCategory] = useState(''); const [filterKeyword, setFilterKeyword] = useState(''); // Pending const [pendingApps, setPendingApps] = useState([]); const [pendingTotal, setPendingTotal] = useState(0); // Coupons const [coupons, setCoupons] = useState([]); const [couponModal, setCouponModal] = useState(false); const [editingCoupon, setEditingCoupon] = useState(null); // Reviews const [reviews, setReviews] = useState([]); // Edit modal const [editApp, setEditApp] = useState(null); const loadOverview = useCallback(async () => { setLoading(true); try { const res = await adminAPI.getMarketOverview(); if (res?.code === 0) setOverview(res.data); const topRes = await adminAPI.getTopApps(topMetric, 10); if (topRes?.code === 0) setTopApps(topRes.data || []); } catch (e) { console.error(e); } setLoading(false); }, [topMetric]); const loadApps = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams(); if (filterStatus) params.set('status', filterStatus); if (filterType) params.set('app_type', filterType); if (filterCategory) params.set('category', filterCategory); if (filterKeyword) params.set('keyword', filterKeyword); params.set('page', String(appsPage)); params.set('page_size', '20'); const res = await adminAPI.getAdminMarketApps(params.toString()); if (res?.code === 0) { const d = res.data as any; setApps(d?.items || []); setAppsTotal(d?.total || 0); } } catch (e) { console.error(e); } setLoading(false); }, [filterStatus, filterType, filterCategory, filterKeyword, appsPage]); const loadPending = useCallback(async () => { setLoading(true); try { const res = await adminAPI.getPendingApps(); if (res?.code === 0) { const d = res.data as any; setPendingApps(d?.items || d || []); setPendingTotal(d?.total || (Array.isArray(d) ? d.length : 0)); } } catch (e) { console.error(e); } setLoading(false); }, []); const loadCoupons = useCallback(async () => { setLoading(true); try { const res = await adminAPI.getCoupons(); if (res?.code === 0) { const d = res.data as any; setCoupons(d?.items || d || []); } } catch (e) { console.error(e); } setLoading(false); }, []); const loadReviews = useCallback(async () => { setLoading(true); try { const res = await adminAPI.getRecentReviews(); if (res?.code === 0) setReviews(res.data || []); } catch (e) { console.error(e); } setLoading(false); }, []); useEffect(() => { if (activeTab === 'overview') loadOverview(); else if (activeTab === 'apps') loadApps(); else if (activeTab === 'pending') loadPending(); else if (activeTab === 'coupons') loadCoupons(); else if (activeTab === 'reviews') loadReviews(); }, [activeTab, loadOverview, loadApps, loadPending, loadCoupons, loadReviews]); // Actions const handleApprove = async (id: string) => { if (!confirm('确认审核通过?')) return; try { await adminAPI.approveApp(id); loadPending(); loadOverview(); } catch (e) { alert('操作失败'); } }; const handleReject = async (id: string) => { const reason = prompt('拒绝原因:', ''); if (reason === null) return; try { await adminAPI.rejectApp(id + '?reason=' + encodeURIComponent(reason)); loadPending(); } catch (e) { alert('操作失败'); } }; const handleToggleFeatured = async (app: MarketAppItem) => { try { await adminAPI.setFeaturedApp(app.id, !app.featured, app.featured ? 0 : 1); loadApps(); } catch (e) { alert('操作失败'); } }; const handleDeleteApp = async (id: string, name: string) => { if (!confirm(`确认删除应用「${name}」?此操作不可恢复!`)) return; try { await adminAPI.deleteMarketApp(id); loadApps(); } catch (e) { alert('操作失败'); } }; const handleDeleteCoupon = async (id: string) => { if (!confirm('确认删除此优惠券?')) return; try { await adminAPI.deleteCoupon(id); loadCoupons(); } catch (e) { alert('操作失败'); } }; const tabs = [ { key: 'overview', label: '数据总览', icon: 'chart' }, { key: 'apps', label: '应用管理', icon: 'store' }, { key: 'pending', label: `待审核${pendingTotal > 0 ? `(${pendingTotal})` : ''}`, icon: 'eye' }, { key: 'coupons', label: '优惠券', icon: 'tag' }, { key: 'reviews', label: '用户评价', icon: 'star' }, { key: 'mcp', label: 'MCP工具', icon: 'tool' }, ] as const; return (
{/* Header */}

市场管理

管理应用市场、审核提交、分析数据、配置优惠券

{/* Tabs */}
{tabs.map(tab => ( ))}
{/* Loading */} {loading && (
加载中...
)} {/* Content */} {!loading && activeTab === 'overview' && overview && ( )} {!loading && activeTab === 'apps' && ( )} {!loading && activeTab === 'pending' && ( )} {!loading && activeTab === 'coupons' && ( { setEditingCoupon(null); setCouponModal(true); }} onEdit={(c) => { setEditingCoupon(c); setCouponModal(true); }} onDelete={handleDeleteCoupon} /> )} {!loading && activeTab === 'reviews' && ( )} {!loading && activeTab === 'mcp' && ( )} {/* Edit Modal */} {editApp && ( setEditApp(null)} onSaved={() => { setEditApp(null); loadApps(); }} /> )} {/* Coupon Modal */} {couponModal && ( setCouponModal(false)} onSaved={() => { setCouponModal(false); loadCoupons(); }} /> )}
); }; // ============================================================ // Overview Component // ============================================================ const MarketOverview: React.FC<{ overview: OverviewData; topApps: MarketAppItem[]; topMetric: string; onMetricChange: (v: string) => void }> = ({ overview, topApps, topMetric, onMetricChange }) => { const stats = [ { label: '总应用数', value: overview.total_apps, sub: `${overview.published} 已发布`, color: 'var(--color-primary)' }, { label: '总安装量', value: overview.total_installs, sub: `${overview.pending} 待审核`, color: 'var(--color-success)' }, { label: '总收入', value: `¥${overview.total_revenue.toFixed(2)}`, sub: `${overview.total_purchases} 笔购买`, color: 'var(--color-warning)' }, { label: '平均评分', value: overview.overall_rating.toFixed(1), sub: `${overview.total_reviews} 条评价`, color: 'var(--color-danger)' }, ]; const maxTrend = Math.max(...overview.trend_7d.map(t => Math.max(t.new_apps, t.new_installs)), 1); return (
{/* Stat cards */}
{stats.map((s, i) => (
{s.label}
{s.value}
{s.sub}
))}
{/* 7-day trend */}

7日趋势

{overview.trend_7d.map((t, i) => (
{t.date.slice(5)}
))}
新应用 新安装
{/* Category distribution + Top apps */}
{/* Categories */}

分类分布

{Object.entries(overview.by_category).map(([cat, cnt]) => (
{CATEGORY_LABELS[cat] || cat} {cnt}
))} {Object.keys(overview.by_category).length === 0 && (
暂无数据
)}
{/* Top apps */}

热门应用

{topApps.map((app, i) => (
#{i + 1}
{app.name}
{TYPE_LABELS[app.type] || app.type} · {CATEGORY_LABELS[app.category] || app.category}
{topMetric === 'installs' && `${app.install_count}`} {topMetric === 'rating' && `${app.rating_avg.toFixed(1)}`} {topMetric === 'reviews' && `${app.review_count}`} {topMetric === 'revenue' && `¥${(app as any).revenue?.toFixed(0) || 0}`}
))} {topApps.length === 0 && (
暂无数据
)}
); }; // ============================================================ // Apps Management Component // ============================================================ const AppsManagement: React.FC<{ apps: MarketAppItem[]; total: number; page: number; onPageChange: (p: number) => void; filterStatus: string; setFilterStatus: (v: string) => void; filterType: string; setFilterType: (v: string) => void; filterCategory: string; setFilterCategory: (v: string) => void; filterKeyword: string; setFilterKeyword: (v: string) => void; onEdit: (app: MarketAppItem) => void; onToggleFeatured: (app: MarketAppItem) => void; onDelete: (id: string, name: string) => void; }> = ({ apps, total, page, onPageChange, filterStatus, setFilterStatus, filterType, setFilterType, filterCategory, setFilterCategory, filterKeyword, setFilterKeyword, onEdit, onToggleFeatured, onDelete }) => { const totalPages = Math.ceil(total / 20); return (
{/* Filters */}
setFilterKeyword(e.target.value)} style={{ flex: 1, minWidth: '200px', padding: '8px 12px', borderRadius: '8px', border: '1px solid var(--color-border)', background: 'var(--color-bg-secondary)', color: 'var(--color-text-primary)' }} />
{/* Table */}
{['应用', '类型', '分类', '价格', '安装', '评分', '状态', '推荐', '操作'].map(h => ( ))} {apps.map(app => ( ))}
{h}
{app.icon && { (e.target as HTMLImageElement).style.display = 'none'; }} />}
{app.name}
{app.publisher_name || '—'}
{TYPE_LABELS[app.type] || app.type} {CATEGORY_LABELS[app.category] || app.category} {app.price === 0 ? 免费 : `¥${app.price}`} {app.is_member_free && 会员免费} {app.install_count} {app.rating_avg > 0 ? `★ ${app.rating_avg.toFixed(1)} (${app.review_count})` : '—'} {STATUS_LABELS[app.status] || app.status}
{apps.length === 0 && (
暂无应用
)}
{/* Pagination */} {totalPages > 1 && (
{page} / {totalPages}
)}
); }; // ============================================================ // Pending Apps Component // ============================================================ const PendingApps: React.FC<{ apps: MarketAppItem[]; onApprove: (id: string) => void; onReject: (id: string) => void }> = ({ apps, onApprove, onReject }) => (
{apps.length === 0 ? (

没有待审核的应用

) : (
{apps.map(app => (
{app.icon ? ( { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : (
)}

{app.name}

{TYPE_LABELS[app.type] || app.type} by {app.publisher_name || 'Unknown'}

{app.description || '无描述'}

{app.skill_content && (
查看 Skill 内容
                    {app.skill_content.slice(0, 500)}{app.skill_content.length > 500 ? '...' : ''}
                  
)}
))}
)}
); // ============================================================ // Coupons Component // ============================================================ const CouponsManagement: React.FC<{ coupons: CouponItem[]; onAdd: () => void; onEdit: (c: CouponItem) => void; onDelete: (id: string) => void; }> = ({ coupons, onAdd, onEdit, onDelete }) => (
{['优惠券码', '类型', '面值', '最低消费', '有效期', '使用情况', '状态', '操作'].map(h => ( ))} {coupons.map(c => ( ))}
{h}
{c.code} {c.type === 'fixed' ? '固定金额' : '百分比'} {c.type === 'fixed' ? `¥${c.value}` : `${c.value}%`} ¥{c.min_amount} {c.valid_from?.slice(0, 10) || '—'} ~ {c.valid_to?.slice(0, 10) || '长期'} {c.used_count} / {c.total_count} {c.status === 'active' ? '有效' : '已停用'}
{coupons.length === 0 && (
暂无优惠券
)}
); // ============================================================ // Reviews List Component // ============================================================ const ReviewsList: React.FC<{ reviews: ReviewItem[] }> = ({ reviews }) => (
{reviews.length === 0 ? (

暂无用户评价

) : ( reviews.map(r => (
{r.avatar ? ( ) : (
{r.username?.charAt(0).toUpperCase() || 'U'}
)} {r.username} {'★'.repeat(r.rating)}{'☆'.repeat(5 - r.rating)} on {r.app_name} {r.created_at?.slice(0, 16)}

{r.content}

)) )}
); // ============================================================ // Edit App Modal // ============================================================ const EditAppModal: React.FC<{ app: MarketAppItem; onClose: () => void; onSaved: () => void }> = ({ app, onClose, onSaved }) => { const [form, setForm] = useState({ name: app.name, description: app.description || '', category: app.category || 'other', tags: app.tags || '', price: app.price, is_member_free: app.is_member_free, featured: app.featured, featured_order: app.featured_order, status: app.status, }); const [saving, setSaving] = useState(false); const handleSave = async () => { setSaving(true); try { await adminAPI.updateMarketApp(app.id, form); onSaved(); } catch (e) { alert('保存失败'); } setSaving(false); }; return (
e.stopPropagation()}>

编辑应用

setForm({ ...form, name: e.target.value })} style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid var(--color-border)', background: 'var(--color-bg-secondary)', color: 'var(--color-text-primary)' }} />