chore: normalize line endings to LF via .gitattributes
Root-cause fix for mixed CRLF/LF in the repo (no .gitattributes previously). Adds '* text=auto eol=lf' and explicit binary rules so all future checkouts are LF. Renormalizes all tracked text files (no logical content change). Backup tags: image qihuaiflow:backup-lineend-20260725-121632, git backup-lineend-pre-20260725-121632.
This commit is contained in:
1 parent
2a7bb875f9
commit
771c8456b8
39 files changed
+34810
-34777
No files matched your search
+388
-388
@@ -1,388 +1,388 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { adminAPI } from '@/api';
|
||||
import { 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>
|
||||
);
|
||||
|
||||
// Phase 4.1: 安检状态徽章
|
||||
function SecurityBadge({ scan }: { scan?: any }) {
|
||||
if (!scan || !scan.status) return <span style={{ color: 'var(--color-text-disabled)' }}>未扫描</span>;
|
||||
const map: Record<string, { label: string; bg: string; fg: string }> = {
|
||||
pass: { label: '安检通过', bg: '#16a34a1A', fg: '#16a34a' },
|
||||
warn: { label: '安检注意', bg: '#d977061A', fg: '#d97706' },
|
||||
fail: { label: '安检风险', bg: '#dc26261A', fg: '#dc2626' },
|
||||
};
|
||||
const m = map[scan.status] || map.warn;
|
||||
return (
|
||||
<span style={{ background: m.bg, color: m.fg, padding: '2px 8px', borderRadius: 'var(--radius-full)', fontSize: 'var(--text-xs)', fontWeight: 600 }}>
|
||||
{m.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const mapApp = (a: any): MarketApp => ({
|
||||
id: a.id, name: a.name, type: a.type, source: a.source,
|
||||
publisherId: a.publisher_id, publisherName: a.publisher_name || '未知',
|
||||
description: a.description || '', icon: a.icon || '?',
|
||||
price: a.price || 0, isMemberFree: a.is_member_free || false,
|
||||
joinPromotion: a.join_promotion || false, status: a.status || 'published',
|
||||
ratingAvg: a.rating_avg || 0, reviewCount: a.review_count || 0,
|
||||
installCount: a.install_count || 0, createdAt: a.created_at || '',
|
||||
version: a.version || '1.0.0', verified: a.verified || false,
|
||||
securityScan: a.security_scan || null, dependencies: a.dependencies || null,
|
||||
});
|
||||
|
||||
const Market: React.FC = () => {
|
||||
const [reviewApps, setReviewApps] = useState<MarketApp[]>([]);
|
||||
const [officialApps, setOfficialApps] = useState<MarketApp[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'review' | 'coupon' | 'official'>('review');
|
||||
const [showCouponModal, setShowCouponModal] = useState(false);
|
||||
const [coupons] = useState<Coupon[]>(mockCoupons);
|
||||
|
||||
const [couponForm, setCouponForm] = useState({
|
||||
code: '', type: 'fixed' as 'fixed' | 'percent', value: 0, minAmount: 0,
|
||||
validFrom: '', validTo: '',
|
||||
});
|
||||
|
||||
// Phase 4.1: 版本历史弹窗
|
||||
const [versionsApp, setVersionsApp] = useState<MarketApp | null>(null);
|
||||
const [versions, setVersions] = useState<any[]>([]);
|
||||
const [loadingVersions, setLoadingVersions] = useState(false);
|
||||
|
||||
const loadReview = () => {
|
||||
setLoading(true);
|
||||
adminAPI.getPendingApps().then((res: any) => {
|
||||
if (res && Array.isArray(res)) setReviewApps(res.map(mapApp));
|
||||
else if (res?.code === 0 && Array.isArray(res.data)) setReviewApps(res.data.map(mapApp));
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const loadOfficial = () => {
|
||||
setLoading(true);
|
||||
adminAPI.getAdminMarketApps('status=published').then((res: any) => {
|
||||
const items = (res?.data?.items || res?.items || []).map(mapApp).filter((a: MarketApp) => a.source === 'official');
|
||||
setOfficialApps(items);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'review') loadReview();
|
||||
else if (activeTab === 'official') loadOfficial();
|
||||
}, [activeTab]);
|
||||
|
||||
const handleApprove = (id: string) => {
|
||||
adminAPI.approveApp(id).then(() => loadReview()).catch(() => alert('审核失败'));
|
||||
};
|
||||
|
||||
const handleReject = (id: string) => {
|
||||
adminAPI.rejectApp(id).then(() => loadReview()).catch(() => alert('拒绝失败'));
|
||||
};
|
||||
|
||||
const handleRemoveOfficial = (id: string) => {
|
||||
if (!confirm('确认下架该官方应用?')) return;
|
||||
adminAPI.rejectApp(id).then(() => loadOfficial()).catch(() => alert('下架失败'));
|
||||
};
|
||||
|
||||
const handleRescan = (id: string) => {
|
||||
adminAPI.rescanApp(id).then((res: any) => {
|
||||
alert('重新安检完成:' + (res?.status || 'unknown'));
|
||||
loadOfficial();
|
||||
}).catch(() => alert('安检失败'));
|
||||
};
|
||||
|
||||
const openVersions = (app: MarketApp) => {
|
||||
setVersionsApp(app);
|
||||
setLoadingVersions(true);
|
||||
setVersions([]);
|
||||
adminAPI.getAppVersions(app.id).then((res: any) => {
|
||||
setVersions(res?.items || []);
|
||||
}).catch(() => {}).finally(() => setLoadingVersions(false));
|
||||
};
|
||||
|
||||
const handleRollback = (appId: string, versionId: string) => {
|
||||
if (!confirm('确认回滚到该版本?当前配置将被覆盖。')) return;
|
||||
adminAPI.rollbackApp(appId, versionId).then(() => {
|
||||
alert('已回滚');
|
||||
openVersions({ ...versionsApp!, id: appId } as MarketApp);
|
||||
loadOfficial();
|
||||
}).catch(() => alert('回滚失败'));
|
||||
};
|
||||
|
||||
const formatDate = (d: string) => {
|
||||
if (!d) return '-';
|
||||
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: reviewApps.length },
|
||||
{ key: 'coupon' as const, label: '优惠券管理', count: coupons.length },
|
||||
{ key: 'official' as const, label: '官方应用管理', count: officialApps.length },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
||||
<h1 className="admin-page-title">AI技能审核管理</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>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={loadReview}>刷新</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>
|
||||
{reviewApps.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><SecurityBadge scan={app.securityScan} /></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>
|
||||
))}
|
||||
{reviewApps.length === 0 && !loading && (
|
||||
<tr>
|
||||
<td colSpan={7} style={{ textAlign: 'center', color: 'var(--color-text-disabled)', padding: 'var(--space-3xl)' }}>
|
||||
暂无待审核应用
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{loading && (
|
||||
<tr><td colSpan={7} style={{ textAlign: 'center', 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><span style={{ color: 'var(--color-text-disabled)' }}>只读</span></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>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={loadOfficial}>刷新</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>
|
||||
{officialApps.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)', fontSize: 'var(--text-xs)' }}>v{app.version}</td>
|
||||
<td>
|
||||
{app.verified
|
||||
? <span style={{ color: '#16a34a', fontWeight: 600 }}>✓ 已认证</span>
|
||||
: <span style={{ color: 'var(--color-text-disabled)' }}>未认证</span>}
|
||||
</td>
|
||||
<td><SecurityBadge scan={app.securityScan} /></td>
|
||||
<td>{app.installCount.toLocaleString()}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleRescan(app.id)}>重新安检</button>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => openVersions(app)}>版本历史</button>
|
||||
<button className="admin-btn admin-btn-danger admin-btn-sm" onClick={() => handleRemoveOfficial(app.id)}>下架</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{officialApps.length === 0 && !loading && (
|
||||
<tr><td colSpan={7} style={{ textAlign: 'center', color: 'var(--color-text-disabled)', padding: 'var(--space-3xl)' }}>暂无官方应用</td></tr>
|
||||
)}
|
||||
{loading && (
|
||||
<tr><td colSpan={7} style={{ textAlign: 'center', padding: 'var(--space-3xl)' }}>加载中...</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Version History Modal */}
|
||||
{versionsApp && (
|
||||
<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={() => setVersionsApp(null)}>
|
||||
<div style={{
|
||||
background: '#FFFFFF', borderRadius: 'var(--radius-2xl)', width: 520, 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-lg)' }}>
|
||||
版本历史 · {versionsApp.name}
|
||||
</h3>
|
||||
<p style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', marginBottom: 'var(--space-2xl)' }}>
|
||||
当前版本:v{versionsApp.version}
|
||||
</p>
|
||||
{loadingVersions ? (
|
||||
<p style={{ color: 'var(--color-text-disabled)' }}>加载中...</p>
|
||||
) : versions.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-disabled)' }}>暂无历史版本</p>
|
||||
) : (
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr><th>版本</th><th>安检</th><th>创建时间</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((v: any) => (
|
||||
<tr key={v.id}>
|
||||
<td style={{ fontFamily: 'var(--font-mono)' }}>v{v.version}</td>
|
||||
<td><SecurityBadge scan={v.security_scan} /></td>
|
||||
<td style={{ fontSize: 'var(--text-xs)' }}>{formatDate(v.created_at)}</td>
|
||||
<td>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleRollback(versionsApp.id, v.id)}>
|
||||
回滚到此版本
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<div className="admin-form-actions" style={{ marginTop: 'var(--space-2xl)' }}>
|
||||
<button className="admin-btn admin-btn-outline" onClick={() => setVersionsApp(null)}>关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Market;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { adminAPI } from '@/api';
|
||||
import { 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>
|
||||
);
|
||||
|
||||
// Phase 4.1: 安检状态徽章
|
||||
function SecurityBadge({ scan }: { scan?: any }) {
|
||||
if (!scan || !scan.status) return <span style={{ color: 'var(--color-text-disabled)' }}>未扫描</span>;
|
||||
const map: Record<string, { label: string; bg: string; fg: string }> = {
|
||||
pass: { label: '安检通过', bg: '#16a34a1A', fg: '#16a34a' },
|
||||
warn: { label: '安检注意', bg: '#d977061A', fg: '#d97706' },
|
||||
fail: { label: '安检风险', bg: '#dc26261A', fg: '#dc2626' },
|
||||
};
|
||||
const m = map[scan.status] || map.warn;
|
||||
return (
|
||||
<span style={{ background: m.bg, color: m.fg, padding: '2px 8px', borderRadius: 'var(--radius-full)', fontSize: 'var(--text-xs)', fontWeight: 600 }}>
|
||||
{m.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const mapApp = (a: any): MarketApp => ({
|
||||
id: a.id, name: a.name, type: a.type, source: a.source,
|
||||
publisherId: a.publisher_id, publisherName: a.publisher_name || '未知',
|
||||
description: a.description || '', icon: a.icon || '?',
|
||||
price: a.price || 0, isMemberFree: a.is_member_free || false,
|
||||
joinPromotion: a.join_promotion || false, status: a.status || 'published',
|
||||
ratingAvg: a.rating_avg || 0, reviewCount: a.review_count || 0,
|
||||
installCount: a.install_count || 0, createdAt: a.created_at || '',
|
||||
version: a.version || '1.0.0', verified: a.verified || false,
|
||||
securityScan: a.security_scan || null, dependencies: a.dependencies || null,
|
||||
});
|
||||
|
||||
const Market: React.FC = () => {
|
||||
const [reviewApps, setReviewApps] = useState<MarketApp[]>([]);
|
||||
const [officialApps, setOfficialApps] = useState<MarketApp[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'review' | 'coupon' | 'official'>('review');
|
||||
const [showCouponModal, setShowCouponModal] = useState(false);
|
||||
const [coupons] = useState<Coupon[]>(mockCoupons);
|
||||
|
||||
const [couponForm, setCouponForm] = useState({
|
||||
code: '', type: 'fixed' as 'fixed' | 'percent', value: 0, minAmount: 0,
|
||||
validFrom: '', validTo: '',
|
||||
});
|
||||
|
||||
// Phase 4.1: 版本历史弹窗
|
||||
const [versionsApp, setVersionsApp] = useState<MarketApp | null>(null);
|
||||
const [versions, setVersions] = useState<any[]>([]);
|
||||
const [loadingVersions, setLoadingVersions] = useState(false);
|
||||
|
||||
const loadReview = () => {
|
||||
setLoading(true);
|
||||
adminAPI.getPendingApps().then((res: any) => {
|
||||
if (res && Array.isArray(res)) setReviewApps(res.map(mapApp));
|
||||
else if (res?.code === 0 && Array.isArray(res.data)) setReviewApps(res.data.map(mapApp));
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const loadOfficial = () => {
|
||||
setLoading(true);
|
||||
adminAPI.getAdminMarketApps('status=published').then((res: any) => {
|
||||
const items = (res?.data?.items || res?.items || []).map(mapApp).filter((a: MarketApp) => a.source === 'official');
|
||||
setOfficialApps(items);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'review') loadReview();
|
||||
else if (activeTab === 'official') loadOfficial();
|
||||
}, [activeTab]);
|
||||
|
||||
const handleApprove = (id: string) => {
|
||||
adminAPI.approveApp(id).then(() => loadReview()).catch(() => alert('审核失败'));
|
||||
};
|
||||
|
||||
const handleReject = (id: string) => {
|
||||
adminAPI.rejectApp(id).then(() => loadReview()).catch(() => alert('拒绝失败'));
|
||||
};
|
||||
|
||||
const handleRemoveOfficial = (id: string) => {
|
||||
if (!confirm('确认下架该官方应用?')) return;
|
||||
adminAPI.rejectApp(id).then(() => loadOfficial()).catch(() => alert('下架失败'));
|
||||
};
|
||||
|
||||
const handleRescan = (id: string) => {
|
||||
adminAPI.rescanApp(id).then((res: any) => {
|
||||
alert('重新安检完成:' + (res?.status || 'unknown'));
|
||||
loadOfficial();
|
||||
}).catch(() => alert('安检失败'));
|
||||
};
|
||||
|
||||
const openVersions = (app: MarketApp) => {
|
||||
setVersionsApp(app);
|
||||
setLoadingVersions(true);
|
||||
setVersions([]);
|
||||
adminAPI.getAppVersions(app.id).then((res: any) => {
|
||||
setVersions(res?.items || []);
|
||||
}).catch(() => {}).finally(() => setLoadingVersions(false));
|
||||
};
|
||||
|
||||
const handleRollback = (appId: string, versionId: string) => {
|
||||
if (!confirm('确认回滚到该版本?当前配置将被覆盖。')) return;
|
||||
adminAPI.rollbackApp(appId, versionId).then(() => {
|
||||
alert('已回滚');
|
||||
openVersions({ ...versionsApp!, id: appId } as MarketApp);
|
||||
loadOfficial();
|
||||
}).catch(() => alert('回滚失败'));
|
||||
};
|
||||
|
||||
const formatDate = (d: string) => {
|
||||
if (!d) return '-';
|
||||
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: reviewApps.length },
|
||||
{ key: 'coupon' as const, label: '优惠券管理', count: coupons.length },
|
||||
{ key: 'official' as const, label: '官方应用管理', count: officialApps.length },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
||||
<h1 className="admin-page-title">AI技能审核管理</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>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={loadReview}>刷新</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>
|
||||
{reviewApps.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><SecurityBadge scan={app.securityScan} /></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>
|
||||
))}
|
||||
{reviewApps.length === 0 && !loading && (
|
||||
<tr>
|
||||
<td colSpan={7} style={{ textAlign: 'center', color: 'var(--color-text-disabled)', padding: 'var(--space-3xl)' }}>
|
||||
暂无待审核应用
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{loading && (
|
||||
<tr><td colSpan={7} style={{ textAlign: 'center', 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><span style={{ color: 'var(--color-text-disabled)' }}>只读</span></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>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={loadOfficial}>刷新</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>
|
||||
{officialApps.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)', fontSize: 'var(--text-xs)' }}>v{app.version}</td>
|
||||
<td>
|
||||
{app.verified
|
||||
? <span style={{ color: '#16a34a', fontWeight: 600 }}>✓ 已认证</span>
|
||||
: <span style={{ color: 'var(--color-text-disabled)' }}>未认证</span>}
|
||||
</td>
|
||||
<td><SecurityBadge scan={app.securityScan} /></td>
|
||||
<td>{app.installCount.toLocaleString()}</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleRescan(app.id)}>重新安检</button>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => openVersions(app)}>版本历史</button>
|
||||
<button className="admin-btn admin-btn-danger admin-btn-sm" onClick={() => handleRemoveOfficial(app.id)}>下架</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{officialApps.length === 0 && !loading && (
|
||||
<tr><td colSpan={7} style={{ textAlign: 'center', color: 'var(--color-text-disabled)', padding: 'var(--space-3xl)' }}>暂无官方应用</td></tr>
|
||||
)}
|
||||
{loading && (
|
||||
<tr><td colSpan={7} style={{ textAlign: 'center', padding: 'var(--space-3xl)' }}>加载中...</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Version History Modal */}
|
||||
{versionsApp && (
|
||||
<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={() => setVersionsApp(null)}>
|
||||
<div style={{
|
||||
background: '#FFFFFF', borderRadius: 'var(--radius-2xl)', width: 520, 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-lg)' }}>
|
||||
版本历史 · {versionsApp.name}
|
||||
</h3>
|
||||
<p style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', marginBottom: 'var(--space-2xl)' }}>
|
||||
当前版本:v{versionsApp.version}
|
||||
</p>
|
||||
{loadingVersions ? (
|
||||
<p style={{ color: 'var(--color-text-disabled)' }}>加载中...</p>
|
||||
) : versions.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-disabled)' }}>暂无历史版本</p>
|
||||
) : (
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr><th>版本</th><th>安检</th><th>创建时间</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((v: any) => (
|
||||
<tr key={v.id}>
|
||||
<td style={{ fontFamily: 'var(--font-mono)' }}>v{v.version}</td>
|
||||
<td><SecurityBadge scan={v.security_scan} /></td>
|
||||
<td style={{ fontSize: 'var(--text-xs)' }}>{formatDate(v.created_at)}</td>
|
||||
<td>
|
||||
<button className="admin-btn admin-btn-outline admin-btn-sm" onClick={() => handleRollback(versionsApp.id, v.id)}>
|
||||
回滚到此版本
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<div className="admin-form-actions" style={{ marginTop: 'var(--space-2xl)' }}>
|
||||
<button className="admin-btn admin-btn-outline" onClick={() => setVersionsApp(null)}>关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Market;
|
||||
Reference in new issue
Block a user