diff --git a/frontend/src/pages/admin/Billing.tsx b/frontend/src/pages/admin/Billing.tsx index f5d4a19..1c5c7dd 100644 --- a/frontend/src/pages/admin/Billing.tsx +++ b/frontend/src/pages/admin/Billing.tsx @@ -57,25 +57,25 @@ const Billing: React.FC = () => { useEffect(() => { if (tab === 'packages') fetchPackages(); }, [tab, fetchPackages]); useEffect(() => { if (tab === 'transactions') fetchTransactions(); }, [tab, fetchTransactions, page]); - const typeColors: Record = { per_token: '#6366f1', per_call: '#3b82f6', per_month: '#10b981', hybrid: '#f59e0b' }; + const typeColors: Record = { per_token: '#6C63FF', per_call: '#3b82f6', per_month: '#10b981', hybrid: '#f59e0b' }; const txColors: Record = { recharge: '#10b981', consumption: '#ef4444', refund: '#f59e0b', gift: '#ec4899' }; return ( -
+

计费与支付管理

{/* Stats */} {stats && ( -
+
{[ { label: '总收入(元)', value: stats.total_revenue?.toFixed(2) || '0.00', color: '#10b981' }, { label: '总消费(元)', value: stats.total_consumption?.toFixed(2) || '0.00', color: '#ef4444' }, { label: '交易笔数', value: stats.total_transactions || 0, color: '#3b82f6' }, { label: '待付账单', value: stats.pending_invoices || 0, color: '#f59e0b' }, ].map((s, i) => ( -
+
{s.value}
-
{s.label}
+
{s.label}
))}
@@ -86,14 +86,14 @@ const Billing: React.FC = () => { {([['overview', '概览'], ['rules', '计费规则'], ['packages', '充值套餐'], ['transactions', '交易记录']] as const).map(([k, label]) => ( + background: tab === k ? '#6C63FF' : '#F0F2F8', color: tab === k ? '#fff' : '#6B7280', fontWeight: 600, fontSize: '14px' }}>{label} ))}
{/* Overview */} {tab === 'overview' && stats && ( -
-

近7天收入趋势

+
+

近7天收入趋势

{stats.daily_revenue_7d && stats.daily_revenue_7d.length > 0 ? (
{stats.daily_revenue_7d.map((d, i) => { @@ -101,37 +101,37 @@ const Billing: React.FC = () => { const h = (d.revenue / max) * 160; return (
-
¥{d.revenue.toFixed(0)}
-
-
{d.date.substring(5)}
+
¥{d.revenue.toFixed(0)}
+
+
{d.date.substring(5)}
); })}
- ) :
暂无收入数据
} + ) :
暂无收入数据
}
)} {/* Rules */} {tab === 'rules' && ( - - +
+ {['规则名称', '计费类型', '关联模型', '单价', '免费额度', '状态'].map(h => ( - + ))} {rules.map(r => ( - + - + - + ))} @@ -141,22 +141,22 @@ const Billing: React.FC = () => { {/* Packages */} {tab === 'packages' && ( -
+
{packages.map(p => ( -
+
{p.is_popular === 1 && ( -
推荐
+
推荐
)}

{p.name}

-

{p.description || ''}

-
¥{p.price}
- {p.original_price &&
¥{p.original_price}
} -
-
主额度: {p.tokens.toLocaleString()} tokens
+

{p.description || ''}

+
¥{p.price}
+ {p.original_price &&
¥{p.original_price}
} +
+
主额度: {p.tokens.toLocaleString()} tokens
{p.bonus_tokens > 0 &&
赠送: {p.bonus_tokens.toLocaleString()} tokens
}
- {p.status === 'active' ? '● 在售' : '○ 已停用'} + {p.status === 'active' ? '● 在售' : '○ 已停用'}
))} @@ -166,38 +166,38 @@ const Billing: React.FC = () => { {/* Transactions */} {tab === 'transactions' && ( <> - {loading ?
加载中...
: ( -
{h}{h}
{r.name} {r.billing_type} {r.model_name || '通用'}{r.model_name || '通用'} ¥{r.rate.toFixed(6)}{r.free_quota?.toLocaleString() || 0}{r.free_quota?.toLocaleString() || 0} - {r.status === 'active' ? '启用' : '停用'} + {r.status === 'active' ? '启用' : '停用'}
- + {loading ?
加载中...
: ( +
+ {['时间', '用户', '类型', '金额(元)', 'Tokens', '状态', '描述'].map(h => ( - + ))} {transactions.map(t => ( - - + + - + - + ))} - {transactions.length === 0 && } + {transactions.length === 0 && }
{h}{h}
{t.created_at?.substring(0, 16)}
{t.created_at?.substring(0, 16)} {t.username || t.user_id.substring(0, 8)} {t.transaction_type} {t.amount > 0 ? '+' : ''}{t.amount.toFixed(2)}{t.tokens?.toLocaleString() || '-'}{t.tokens?.toLocaleString() || '-'} {t.status} {t.description || '-'}{t.description || '-'}
暂无交易记录
暂无交易记录
)} {total > 20 && ( -
- - 第 {page} 页 - +
+ + 第 {page} 页 +
)} diff --git a/frontend/src/pages/admin/Market.tsx b/frontend/src/pages/admin/Market.tsx index 6d1d146..14cd818 100644 --- a/frontend/src/pages/admin/Market.tsx +++ b/frontend/src/pages/admin/Market.tsx @@ -1,963 +1,963 @@ -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)' }} /> -
-
- -