- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
343 lines
15 KiB
TypeScript
343 lines
15 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { mockApps, mockCoupons } from '@/mock/data';
|
|
import type { MarketApp, Coupon } from '@/types';
|
|
|
|
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>
|
|
);
|
|
|
|
const Market: React.FC = () => {
|
|
const [apps, setApps] = useState<MarketApp[]>(mockApps);
|
|
const [coupons, setCoupons] = useState<Coupon[]>(mockCoupons);
|
|
const [activeTab, setActiveTab] = useState<'review' | 'coupon' | 'official'>('review');
|
|
const [showCouponModal, setShowCouponModal] = useState(false);
|
|
|
|
const [couponForm, setCouponForm] = useState({
|
|
code: '', type: 'fixed' as 'fixed' | 'percent', value: 0, minAmount: 0,
|
|
validFrom: '', validTo: '',
|
|
});
|
|
|
|
const pendingApps = apps.filter(a => a.status === 'pending');
|
|
const publishedOfficialApps = apps.filter(a => a.source === 'official' && a.status !== 'removed');
|
|
|
|
const handleApprove = (id: string) => {
|
|
setApps(prev => prev.map(a => a.id === id ? { ...a, status: 'published' as const } : a));
|
|
};
|
|
|
|
const handleReject = (id: string) => {
|
|
setApps(prev => prev.map(a => a.id === id ? { ...a, status: 'removed' as const } : a));
|
|
};
|
|
|
|
const handleToggleOfficial = (id: string) => {
|
|
setApps(prev => prev.map(a => {
|
|
if (a.id === id) {
|
|
const newStatus: MarketApp['status'] = a.status === 'published' ? 'removed' : 'published';
|
|
return { ...a, status: newStatus };
|
|
}
|
|
return a;
|
|
}));
|
|
};
|
|
|
|
const handleCreateCoupon = () => {
|
|
if (!couponForm.code.trim() || !couponForm.validFrom || !couponForm.validTo) return;
|
|
setCoupons(prev => [...prev, {
|
|
id: 'cp' + Date.now(),
|
|
...couponForm,
|
|
status: 'active',
|
|
}]);
|
|
setShowCouponModal(false);
|
|
setCouponForm({ code: '', type: 'fixed', value: 0, minAmount: 0, validFrom: '', validTo: '' });
|
|
};
|
|
|
|
const handleDeleteCoupon = (id: string) => {
|
|
setCoupons(prev => prev.filter(c => c.id !== id));
|
|
};
|
|
|
|
const formatDate = (d: string) => {
|
|
const dt = new Date(d);
|
|
return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
};
|
|
|
|
const tabs = [
|
|
{ key: 'review' as const, label: '待审核应用', count: pendingApps.length },
|
|
{ key: 'coupon' as const, label: '优惠券管理', count: coupons.length },
|
|
{ key: 'official' as const, label: '官方应用管理', count: publishedOfficialApps.length },
|
|
];
|
|
|
|
return (
|
|
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
|
<h1 className="admin-page-title">应用市场审核管理</h1>
|
|
|
|
{/* Tabs */}
|
|
<div style={{ display: 'flex', gap: 'var(--space-sm)', marginBottom: 'var(--space-2xl)', borderBottom: '1px solid var(--color-border)', paddingBottom: 'var(--space-lg)' }}>
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.key}
|
|
onClick={() => setActiveTab(tab.key)}
|
|
style={{
|
|
padding: 'var(--space-sm) var(--space-lg)',
|
|
borderRadius: 'var(--radius-md)',
|
|
fontSize: 'var(--text-sm)', fontWeight: 600,
|
|
background: activeTab === tab.key ? 'var(--color-accent-light)' : 'transparent',
|
|
color: activeTab === tab.key ? 'var(--color-accent)' : 'var(--color-text-secondary)',
|
|
transition: 'all var(--transition-fast)',
|
|
display: 'flex', alignItems: 'center', gap: 'var(--space-sm)',
|
|
}}
|
|
>
|
|
{tab.label}
|
|
{tab.count > 0 && (
|
|
<span style={{
|
|
background: activeTab === tab.key ? 'var(--color-accent)' : 'var(--color-text-disabled)',
|
|
color: '#FFFFFF', padding: '0 6px', borderRadius: 'var(--radius-full)',
|
|
fontSize: 'var(--text-xs)', lineHeight: '18px', minWidth: 18, textAlign: 'center',
|
|
}}>
|
|
{tab.count}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Pending Review Tab */}
|
|
{activeTab === 'review' && (
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title">待审核应用</h3>
|
|
</div>
|
|
<div 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>
|
|
{pendingApps.map(app => (
|
|
<tr key={app.id}>
|
|
<td style={{ fontWeight: 600 }}>{app.name}</td>
|
|
<td>
|
|
<span className={`admin-badge ${app.type === 'agent' ? 'badge-info' : app.type === 'skill' ? 'badge-success' : 'badge-warning'}`}>
|
|
{app.type.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td>{app.publisherName}</td>
|
|
<td style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{app.price} 元</td>
|
|
<td><span className="admin-badge badge-warning">待审核</span></td>
|
|
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(app.createdAt)}</td>
|
|
<td>
|
|
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
|
|
<button className="admin-btn admin-btn-primary admin-btn-sm" onClick={() => handleApprove(app.id)}>
|
|
<SVGIcon size={12}><polyline points="20 6 9 17 4 12"/></SVGIcon>
|
|
通过
|
|
</button>
|
|
<button className="admin-btn admin-btn-danger admin-btn-sm" onClick={() => handleReject(app.id)}>
|
|
<SVGIcon size={12}><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></SVGIcon>
|
|
拒绝
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{pendingApps.length === 0 && (
|
|
<tr>
|
|
<td colSpan={7} style={{ textAlign: 'center', color: 'var(--color-text-disabled)', padding: 'var(--space-3xl)' }}>
|
|
暂无待审核应用
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Coupon Management Tab */}
|
|
{activeTab === 'coupon' && (
|
|
<>
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title">优惠券列表</h3>
|
|
<button className="admin-btn admin-btn-primary admin-btn-sm" onClick={() => setShowCouponModal(true)}>
|
|
<SVGIcon><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></SVGIcon>
|
|
创建优惠券
|
|
</button>
|
|
</div>
|
|
<div 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>
|
|
{coupons.map(c => (
|
|
<tr key={c.id}>
|
|
<td style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{c.code}</td>
|
|
<td>
|
|
<span className={`admin-badge ${c.type === 'fixed' ? 'badge-info' : 'badge-warning'}`}>
|
|
{c.type === 'fixed' ? '固定金额' : '百分比'}
|
|
</span>
|
|
</td>
|
|
<td style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>
|
|
{c.type === 'fixed' ? `${c.value} 元` : `${c.value}%`}
|
|
</td>
|
|
<td>{c.minAmount > 0 ? `${c.minAmount} 元` : '无门槛'}</td>
|
|
<td style={{ fontSize: 'var(--text-xs)' }}>{c.validFrom} 至 {c.validTo}</td>
|
|
<td>
|
|
<span className={`admin-badge ${c.status === 'active' ? 'badge-success' : 'badge-error'}`}>
|
|
{c.status === 'active' ? '有效' : '已过期'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<button className="admin-btn admin-btn-danger admin-btn-sm" onClick={() => handleDeleteCoupon(c.id)}>
|
|
删除
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Official App Management Tab */}
|
|
{activeTab === 'official' && (
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title">官方应用列表</h3>
|
|
</div>
|
|
<div style={{ overflowX: 'auto' }}>
|
|
<table className="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>应用名</th>
|
|
<th>类型</th>
|
|
<th>价格</th>
|
|
<th>会员免费</th>
|
|
<th>安装量</th>
|
|
<th>评分</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{publishedOfficialApps.map(app => (
|
|
<tr key={app.id}>
|
|
<td style={{ fontWeight: 600 }}>{app.name}</td>
|
|
<td>
|
|
<span className={`admin-badge ${app.type === 'agent' ? 'badge-info' : app.type === 'skill' ? 'badge-success' : 'badge-warning'}`}>
|
|
{app.type.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td style={{ fontFamily: 'var(--font-mono)' }}>{app.price} 元</td>
|
|
<td>
|
|
<span className={`admin-badge ${app.isMemberFree ? 'badge-success' : 'badge-neutral'}`}>
|
|
{app.isMemberFree ? '是' : '否'}
|
|
</span>
|
|
</td>
|
|
<td>{app.installCount.toLocaleString()}</td>
|
|
<td>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<SVGIcon size={12}><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></SVGIcon>
|
|
{app.ratingAvg}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span className={`admin-badge ${app.status === 'published' ? 'badge-success' : 'badge-neutral'}`}>
|
|
{app.status === 'published' ? '已上架' : '已下架'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<button
|
|
className={`admin-btn ${app.status === 'published' ? 'admin-btn-danger' : 'admin-btn-outline'} admin-btn-sm`}
|
|
onClick={() => handleToggleOfficial(app.id)}
|
|
>
|
|
{app.status === 'published' ? '下架' : '上架'}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Coupon Modal */}
|
|
{showCouponModal && (
|
|
<div style={{
|
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
|
background: 'rgba(0,0,0,0.4)', zIndex: 'var(--z-modal)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
animation: 'fadeIn var(--transition-normal)',
|
|
}} onClick={() => setShowCouponModal(false)}>
|
|
<div style={{
|
|
background: '#FFFFFF', borderRadius: 'var(--radius-2xl)', width: 460, maxHeight: '90vh', overflow: 'auto',
|
|
padding: 'var(--space-3xl)', boxShadow: 'var(--shadow-xl)',
|
|
}} onClick={e => e.stopPropagation()}>
|
|
<h3 style={{ fontSize: 'var(--text-lg)', fontWeight: 700, marginBottom: 'var(--space-2xl)' }}>创建优惠券</h3>
|
|
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">兑换码</label>
|
|
<input className="admin-form-input" value={couponForm.code} onChange={e => setCouponForm({ ...couponForm, code: e.target.value })} placeholder="例如: NEW2026" />
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">类型</label>
|
|
<select className="admin-form-input" value={couponForm.type} onChange={e => setCouponForm({ ...couponForm, type: e.target.value as 'fixed' | 'percent' })}>
|
|
<option value="fixed">固定金额</option>
|
|
<option value="percent">百分比折扣</option>
|
|
</select>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">面值</label>
|
|
<input className="admin-form-input" type="number" value={couponForm.value} onChange={e => setCouponForm({ ...couponForm, value: Number(e.target.value) })} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">最低消费(元)</label>
|
|
<input className="admin-form-input" type="number" value={couponForm.minAmount} onChange={e => setCouponForm({ ...couponForm, minAmount: Number(e.target.value) })} />
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">开始日期</label>
|
|
<input className="admin-form-input" type="date" value={couponForm.validFrom} onChange={e => setCouponForm({ ...couponForm, validFrom: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">结束日期</label>
|
|
<input className="admin-form-input" type="date" value={couponForm.validTo} onChange={e => setCouponForm({ ...couponForm, validTo: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="admin-form-actions">
|
|
<button className="admin-btn admin-btn-primary" onClick={handleCreateCoupon}>创建</button>
|
|
<button className="admin-btn admin-btn-outline" onClick={() => setShowCouponModal(false)}>取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Market;
|