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:
QihuAI Design committed 2026-07-25 12:16:39 +08:00
1 parent 2a7bb875f9
commit 771c8456b8
39 files changed
+34810 -34777

No files matched your search

+388 -388
View File
@@ -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;