feat(frontend): HashRouter->BrowserRouter 干净URL可独立索引 + 静态预渲染(SSR注入#root/robots/sitemap) + logo回顶
This commit is contained in:
1 parent
a1bea39d9f
commit
d404aa4e29
12 files changed
+5859
-5607
No files matched your search
@@ -1,227 +1,385 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { adminAPI } from '@/api';
|
||||
|
||||
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
|
||||
const quickEntries = [
|
||||
{ path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: <SVGIcon><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> },
|
||||
{ path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: <SVGIcon><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></SVGIcon> },
|
||||
{ path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: <SVGIcon><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></SVGIcon> },
|
||||
{ path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: <SVGIcon><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></SVGIcon> },
|
||||
{ path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: <SVGIcon><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></SVGIcon> },
|
||||
{ path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: <SVGIcon><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon> },
|
||||
{ path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: <SVGIcon><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></SVGIcon> },
|
||||
];
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [stats, setStats] = useState({ active_users: 0, enabled_models: 0, today_orders: 0, today_tokens: 0, total_tokens: 0 });
|
||||
const [operationLogs, setOperationLogs] = useState<any[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsRes, logsRes, auditRes] = await Promise.all([
|
||||
adminAPI.getStats(),
|
||||
adminAPI.getRecentLogs(),
|
||||
adminAPI.getRecentAudit(),
|
||||
]);
|
||||
if (statsRes.data) setStats(statsRes.data);
|
||||
if (logsRes.data) setOperationLogs(Array.isArray(logsRes.data) ? logsRes.data : []);
|
||||
if (auditRes.data) setAuditLogs(Array.isArray(auditRes.data) ? auditRes.data : []);
|
||||
} catch (err) {
|
||||
console.error('Dashboard数据加载失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const formatDate = (d: string) => {
|
||||
if (!d) return '-';
|
||||
const dt = new Date(d);
|
||||
if (isNaN(dt.getTime())) return d.slice(0, 19) || '-';
|
||||
return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const formatTokens = (n: number): string => {
|
||||
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
|
||||
return n.toLocaleString('en-US');
|
||||
};
|
||||
|
||||
const statCards = [
|
||||
{ label: '用户总数', value: stats.active_users.toLocaleString(), color: '#6C63FF', bg: '#EEEDFF', icon: <SVGIcon size={22}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon> },
|
||||
{ label: '活跃模型数', value: stats.enabled_models.toString(), color: '#219B5E', bg: '#E6F7EE', icon: <SVGIcon size={22}><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> },
|
||||
{ label: '今日订单数', value: stats.today_orders.toString(), color: '#E8922C', bg: '#FEF4E8', icon: <SVGIcon size={22}><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></SVGIcon> },
|
||||
{ label: '今日Token消耗', value: formatTokens(stats.today_tokens), color: '#3B82F6', bg: '#EBF2FE', icon: <SVGIcon size={22}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon> },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)', padding: 'var(--space-2xl)', textAlign: 'center' }}>
|
||||
<div className="audit-spinner" />
|
||||
<span style={{ marginLeft: 'var(--space-md)', color: 'var(--color-text-tertiary)' }}>加载仪表盘数据...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
||||
<h1 className="admin-page-title">平台驾驶舱</h1>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="admin-stats">
|
||||
{statCards.map((s, i) => (
|
||||
<div key={i} className="admin-stat-card" style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--space-lg)' }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 'var(--radius-lg)',
|
||||
background: s.bg, color: s.color,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{s.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="admin-stat-label">{s.label}</div>
|
||||
<div className="admin-stat-value">{s.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Platform Cumulative Token Stats */}
|
||||
<div className="admin-card" style={{ marginBottom: 'var(--space-xl)' }}>
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">
|
||||
<SVGIcon size={18}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>
|
||||
{' '}平台累计Token消耗
|
||||
</h3>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 'var(--space-xl)' }}>
|
||||
{[
|
||||
{ label: '今日消耗', value: formatTokens(stats.today_tokens), unit: 'Tokens', color: '#3B82F6' },
|
||||
{ label: '累计总消耗', value: formatTokens(stats.total_tokens || 0), unit: 'Tokens', color: '#6C63FF' },
|
||||
{ label: '日均消耗(近30天)', value: formatTokens(Math.round((stats.total_tokens || 0) / 30)), unit: 'Tokens', color: '#219B5E' },
|
||||
{ label: '活跃调用模型', value: stats.enabled_models.toString(), unit: '个', color: '#E8922C' },
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ textAlign: 'center', padding: 'var(--space-lg)' }}>
|
||||
<div style={{ fontSize: 'var(--text-2xl)', fontWeight: 800, color: item.color, fontFamily: 'var(--font-mono)' }}>
|
||||
{item.value}
|
||||
</div>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-secondary)', marginTop: 'var(--space-xs)' }}>
|
||||
{item.label}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--color-text-tertiary)' }}>{item.unit}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Entry Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 'var(--space-lg)', marginBottom: 'var(--space-2xl)' }}>
|
||||
{quickEntries.map((entry) => (
|
||||
<div
|
||||
key={entry.path}
|
||||
onClick={() => navigate(entry.path)}
|
||||
style={{
|
||||
background: '#FFFFFF', borderRadius: 'var(--radius-xl)', border: '1px solid var(--color-border)',
|
||||
padding: 'var(--space-xl)', cursor: 'pointer', transition: 'all var(--transition-fast)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', gap: 'var(--space-sm)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--color-accent)';
|
||||
e.currentTarget.style.boxShadow = 'var(--shadow-md)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--color-border)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<div style={{ color: 'var(--color-accent)', marginBottom: 'var(--space-xs)' }}>{entry.icon}</div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', fontWeight: 600, color: 'var(--color-text-primary)' }}>{entry.label}</div>
|
||||
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{entry.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Recent Operation Logs */}
|
||||
<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>IP</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{operationLogs.length > 0 ? operationLogs.map(log => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ fontWeight: 500 }}>{log.username}</td>
|
||||
<td><span className="admin-badge badge-info">{log.action}</span></td>
|
||||
<td>{log.target}</td>
|
||||
<td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.detail}</td>
|
||||
<td style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{log.ip}</td>
|
||||
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr><td colSpan={6} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无操作日志</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Audit Logs */}
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{auditLogs.length > 0 ? auditLogs.map(log => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ fontWeight: 500 }}>{log.admin_name}</td>
|
||||
<td><span className="admin-badge badge-warning">{log.action}</span></td>
|
||||
<td><span className="admin-badge badge neutral">{log.target_type}</span></td>
|
||||
<td>{log.detail}</td>
|
||||
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr><td colSpan={5} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无审计日志</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { adminAPI } from '@/api';
|
||||
|
||||
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface DashboardStats {
|
||||
total_users: number;
|
||||
active_users: number;
|
||||
total_models: number;
|
||||
enabled_models: number;
|
||||
total_conversations: number;
|
||||
active_conversations: number;
|
||||
total_knowledge_bases: number;
|
||||
total_documents: number;
|
||||
today_orders: number;
|
||||
today_tokens: number;
|
||||
week_tokens: number;
|
||||
total_tokens: number;
|
||||
today_conversations: number;
|
||||
server_time: string;
|
||||
}
|
||||
|
||||
interface DailyStat {
|
||||
date: string;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
const quickEntries = [
|
||||
{ path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: <SVGIcon><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> },
|
||||
{ path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: <SVGIcon><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></SVGIcon> },
|
||||
{ path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: <SVGIcon><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></SVGIcon> },
|
||||
{ path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: <SVGIcon><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></SVGIcon> },
|
||||
{ path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: <SVGIcon><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></SVGIcon> },
|
||||
{ path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: <SVGIcon><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon> },
|
||||
{ path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: <SVGIcon><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></SVGIcon> },
|
||||
];
|
||||
|
||||
// SVG Bar Chart Component
|
||||
const BarChart: React.FC<{ data: DailyStat[] }> = ({ data }) => {
|
||||
if (!data || data.length === 0) return <div style={{ color: '#6B7280', textAlign: 'center', padding: 40 }}>暂无数据</div>;
|
||||
|
||||
const maxVal = Math.max(...data.map(d => d.tokens), 1);
|
||||
const w = 600, h = 200, padLeft = 55, padRight = 10, padTop = 10, padBottom = 25;
|
||||
const barAreaW = w - padLeft - padRight;
|
||||
const barAreaH = h - padTop - padBottom;
|
||||
const barGap = 2;
|
||||
const barW = Math.max(2, (barAreaW / data.length) - barGap);
|
||||
|
||||
// Y axis ticks
|
||||
const yTicks = [0, Math.round(maxVal * 0.25), Math.round(maxVal * 0.5), Math.round(maxVal * 0.75), maxVal];
|
||||
|
||||
const formatK = (n: number) => n >= 10000 ? (n / 1000).toFixed(0) + 'k' : String(n);
|
||||
const formatDate = (d: string) => {
|
||||
const parts = d.split('-');
|
||||
return parts.length >= 2 ? `${parts[1]}/${parts[2]}` : d;
|
||||
};
|
||||
|
||||
return (
|
||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ width: '100%', height: 'auto', maxWidth: 600 }}>
|
||||
{/* Grid lines */}
|
||||
{yTicks.map((tick, i) => {
|
||||
const y = padTop + barAreaH - (tick / maxVal) * barAreaH;
|
||||
return (
|
||||
<g key={i}>
|
||||
<line x1={padLeft} y1={y} x2={w - padRight} y2={y} stroke="rgba(0,0,0,0.06)" strokeWidth="0.5" />
|
||||
<text x={padLeft - 8} y={y + 4} textAnchor="end" fontSize="10" fill="#6B7280">{formatK(tick)}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{/* Bars */}
|
||||
{data.map((d, i) => {
|
||||
const barH = (d.tokens / maxVal) * barAreaH;
|
||||
const x = padLeft + i * (barAreaW / data.length);
|
||||
const y = padTop + barAreaH - barH;
|
||||
return (
|
||||
<g key={i}>
|
||||
<rect x={x + barGap / 2} y={y} width={barW} height={Math.max(barH, 1)} rx="2" fill="#6C63FF" opacity="0.85">
|
||||
<title>{`${d.date}: ${d.tokens.toLocaleString()} tokens`}</title>
|
||||
</rect>
|
||||
{/* X label - every 7th bar */}
|
||||
{i % Math.max(1, Math.floor(data.length / 8)) === 0 && (
|
||||
<text x={x + barW / 2} y={h - 6} textAnchor="middle" fontSize="9" fill="#6B7280">{formatDate(d.date)}</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// Mini Donut for stat cards
|
||||
const MiniDonut: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => {
|
||||
const pct = max > 0 ? Math.min(value / max, 1) : 0;
|
||||
const r = 16, c = 20, circumference = 2 * Math.PI * r;
|
||||
const dash = circumference * pct;
|
||||
return (
|
||||
<svg width={c * 2} height={c * 2} viewBox={`0 0 ${c * 2} ${c * 2}`}>
|
||||
<circle cx={c} cy={c} r={r} fill="none" stroke="rgba(0,0,0,0.06)" strokeWidth="3" />
|
||||
<circle cx={c} cy={c} r={r} fill="none" stroke={color} strokeWidth="3"
|
||||
strokeDasharray={`${dash} ${circumference - dash}`} strokeLinecap="round"
|
||||
transform={`rotate(-90 ${c} ${c})`} style={{ transition: 'stroke-dasharray 0.6s ease' }} />
|
||||
<text x={c} y={c + 4} textAnchor="middle" fontSize="9" fontWeight="600" fill={color}>
|
||||
{Math.round(pct * 100)}%
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
total_users: 0, active_users: 0, total_models: 0, enabled_models: 0,
|
||||
total_conversations: 0, active_conversations: 0, total_knowledge_bases: 0, total_documents: 0,
|
||||
today_orders: 0, today_tokens: 0, week_tokens: 0, total_tokens: 0,
|
||||
today_conversations: 0, server_time: '',
|
||||
});
|
||||
const [dailyStats, setDailyStats] = useState<DailyStat[]>([]);
|
||||
const [operationLogs, setOperationLogs] = useState<any[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsRes, dailyRes, logsRes, auditRes] = await Promise.all([
|
||||
adminAPI.getStats() as any,
|
||||
(adminAPI as any).getDailyStats?.(30) || Promise.resolve({ data: [] }),
|
||||
adminAPI.getRecentLogs() as any,
|
||||
adminAPI.getRecentAudit() as any,
|
||||
]);
|
||||
// Stats: backend returns raw dict (no {code,data} wrapper) — use .data or the response itself
|
||||
const statsData = statsRes?.data || statsRes;
|
||||
if (statsData && typeof statsData === 'object') setStats(statsData);
|
||||
|
||||
// Daily stats: backend returns {code, data: {days, data: [...]}} — access nested .data.data
|
||||
const dailyData = dailyRes?.data?.data || (Array.isArray(dailyRes?.data) ? dailyRes.data : []) || [];
|
||||
setDailyStats(Array.isArray(dailyData) ? dailyData : []);
|
||||
|
||||
// Recent logs: backend returns raw list (no wrapper) — use the response directly if it's an array
|
||||
const logsData = Array.isArray(logsRes) ? logsRes : (Array.isArray(logsRes?.data) ? logsRes.data : []);
|
||||
setOperationLogs(logsData);
|
||||
|
||||
// Recent audit: same as logs
|
||||
const auditData = Array.isArray(auditRes) ? auditRes : (Array.isArray(auditRes?.data) ? auditRes.data : []);
|
||||
setAuditLogs(auditData);
|
||||
} catch (err) {
|
||||
console.error('Dashboard数据加载失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const formatDate = (d: string) => {
|
||||
if (!d) return '-';
|
||||
const dt = new Date(d);
|
||||
if (isNaN(dt.getTime())) return d.slice(0, 19) || '-';
|
||||
return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const formatTokens = (n: number): string => {
|
||||
if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿';
|
||||
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
|
||||
return n.toLocaleString('en-US');
|
||||
};
|
||||
|
||||
const statCardDefs = [
|
||||
{ label: '总用户', value: stats.total_users, sub: `活跃 ${stats.active_users}`, color: '#6C63FF', bg: 'rgba(108,99,255,0.12)', subBg: 'rgba(108,99,255,0.08)',
|
||||
icon: <SVGIcon size={22}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon> },
|
||||
{ label: 'AI模型', value: stats.total_models, sub: `启用 ${stats.enabled_models}`, color: '#219B5E', bg: 'rgba(33,155,94,0.12)', subBg: 'rgba(33,155,94,0.08)',
|
||||
icon: <SVGIcon size={22}><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> },
|
||||
{ label: '对话数', value: stats.total_conversations, sub: `活跃 ${stats.active_conversations}`, color: '#E8922C', bg: 'rgba(232,146,44,0.12)', subBg: 'rgba(232,146,44,0.08)',
|
||||
icon: <SVGIcon size={22}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon> },
|
||||
{ label: '知识库', value: stats.total_knowledge_bases, sub: `文档 ${stats.total_documents}`, color: '#3B82F6', bg: 'rgba(59,130,246,0.12)', subBg: 'rgba(59,130,246,0.08)',
|
||||
icon: <SVGIcon size={22}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon> },
|
||||
{ label: '今日对话', value: stats.today_conversations, sub: `订单 ${stats.today_orders}`, color: '#8B5CF6', bg: 'rgba(139,92,246,0.12)', subBg: 'rgba(139,92,246,0.08)',
|
||||
icon: <SVGIcon size={22}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon> },
|
||||
{ label: '今日Token', value: stats.today_tokens, sub: `本周 ${formatTokens(stats.week_tokens)}`, color: '#EC4899', bg: 'rgba(236,72,153,0.12)', subBg: 'rgba(236,72,153,0.08)', format: true,
|
||||
icon: <SVGIcon size={22}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></SVGIcon> },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)', padding: 'var(--space-2xl)', textAlign: 'center' }}>
|
||||
<div className="audit-spinner" />
|
||||
<span style={{ marginLeft: 'var(--space-md)', color: 'var(--color-text-tertiary)' }}>加载仪表盘数据...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
||||
<h1 className="admin-page-title">平台驾驶舱</h1>
|
||||
|
||||
{/* Stats Cards Row */}
|
||||
<div className="admin-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
|
||||
{statCardDefs.map((s, i) => (
|
||||
<div key={i} className="admin-stat-card" style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 'var(--space-lg)',
|
||||
padding: 'var(--space-lg) var(--space-xl)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 14,
|
||||
background: s.bg, color: s.color,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{s.icon}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="admin-stat-label">{s.label}</div>
|
||||
<div className="admin-stat-value" style={{ fontSize: 'var(--text-xl)' }}>
|
||||
{s.format ? formatTokens(s.value as number) : (s.value as number).toLocaleString()}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 11, color: s.color, marginTop: 2,
|
||||
background: s.subBg || s.bg, padding: '1px 6px', borderRadius: 4,
|
||||
display: 'inline-block',
|
||||
}}>{s.sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 'var(--space-xl)', marginBottom: 'var(--space-xl)' }}>
|
||||
{/* Token Trend Chart */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">
|
||||
<SVGIcon size={18}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>
|
||||
{' '}Token消耗趋势(近30天)
|
||||
</h3>
|
||||
</div>
|
||||
<div style={{ padding: 'var(--space-lg)', display: 'flex', justifyContent: 'center' }}>
|
||||
<BarChart data={dailyStats} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Health */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">
|
||||
<SVGIcon size={18}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>
|
||||
{' '}平台概览
|
||||
</h3>
|
||||
</div>
|
||||
<div style={{ padding: 'var(--space-lg)' }}>
|
||||
{[
|
||||
{ label: '累计Token消耗', value: formatTokens(stats.total_tokens), color: '#6C63FF', pct: 100 },
|
||||
{ label: '模型利用率', value: `${stats.enabled_models}/${stats.total_models}`, color: '#219B5E', pct: stats.total_models > 0 ? stats.enabled_models / stats.total_models : 0 },
|
||||
{ label: '用户活跃率', value: `${stats.active_users}/${stats.total_users}`, color: '#E8922C', pct: stats.total_users > 0 ? stats.active_users / stats.total_users : 0 },
|
||||
{ label: '对话活跃率', value: `${stats.active_conversations}/${stats.total_conversations}`, color: '#3B82F6', pct: stats.total_conversations > 0 ? stats.active_conversations / stats.total_conversations : 0 },
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: i < 3 ? '1px solid rgba(0,0,0,0.06)' : 'none' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<MiniDonut value={item.pct * 100} max={100} color={item.color} />
|
||||
<span style={{ fontSize: 13, color: '#1A1F36' }}>{item.label}</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 15, fontWeight: 700, color: item.color, fontFamily: 'var(--font-mono)' }}>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: 12, fontSize: 11, color: '#6B7280', textAlign: 'center' }}>
|
||||
服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div style={{ marginBottom: 'var(--space-xl)' }}>
|
||||
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12, color: '#1A1F36' }}>快捷入口</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 'var(--space-md)' }}>
|
||||
{quickEntries.map((entry) => (
|
||||
<div
|
||||
key={entry.path}
|
||||
onClick={() => navigate(entry.path)}
|
||||
style={{
|
||||
background: '#FFFFFF', borderRadius: 'var(--radius-lg)', border: '1px solid rgba(0,0,0,0.08)',
|
||||
padding: 'var(--space-md) var(--space-lg)', cursor: 'pointer', transition: 'all 0.2s',
|
||||
display: 'flex', alignItems: 'center', gap: 'var(--space-md)',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#6C63FF';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(108,99,255,0.15)';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--color-border)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
e.currentTarget.style.transform = 'none';
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#6C63FF', flexShrink: 0 }}>{entry.icon}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#1A1F36' }}>{entry.label}</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>{entry.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Logs */}
|
||||
<div className="admin-card" style={{ marginBottom: 'var(--space-xl)' }}>
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">最近操作日志</h3>
|
||||
<span style={{ fontSize: 12, color: '#6B7280' }}>最近10条</span>
|
||||
</div>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>操作</th>
|
||||
<th>目标</th>
|
||||
<th>详情</th>
|
||||
<th>IP</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{operationLogs.length > 0 ? operationLogs.map(log => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ fontWeight: 500 }}>{log.username}</td>
|
||||
<td><span className="admin-badge badge-info">{log.action}</span></td>
|
||||
<td>{log.target}</td>
|
||||
<td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.detail}</td>
|
||||
<td style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{log.ip}</td>
|
||||
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr><td colSpan={6} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无操作日志</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Audit Logs */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h3 className="admin-card-title">最近审计日志</h3>
|
||||
<span style={{ fontSize: 12, color: '#6B7280' }}>最近10条</span>
|
||||
</div>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作人</th>
|
||||
<th>操作类型</th>
|
||||
<th>目标类型</th>
|
||||
<th>详情</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{auditLogs.length > 0 ? auditLogs.map(log => (
|
||||
<tr key={log.id}>
|
||||
<td style={{ fontWeight: 500 }}>{log.admin_name}</td>
|
||||
<td><span className="admin-badge badge-warning">{log.action}</span></td>
|
||||
<td><span className="admin-badge badge neutral">{log.target_type}</span></td>
|
||||
<td>{log.detail}</td>
|
||||
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr><td colSpan={5} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无审计日志</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
+1135
-1080
File diff suppressed because it is too large.
Load diff
@@ -1,704 +1,258 @@
|
||||
/* ===== Landing Page Styles ===== */
|
||||
/* ===== 七虎AI 前台首页 · 遵循 DESIGN.md(亮色/单一靛蓝/渐变/玻璃拟态/SEO) ===== */
|
||||
/* LandingPage 不在 FrontLayout 壳内,而 global.css 全局锁死了 html/body/#root 的滚动;
|
||||
故本页自成一个可滚动容器,避免内容超出 100vh 被裁切且无法翻阅。 */
|
||||
.lp { color: var(--color-text-primary); background: var(--color-bg-primary); height: 100vh; overflow-y: auto; overflow-x: hidden; }
|
||||
|
||||
.landing-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: var(--color-bg-primary);
|
||||
z-index: var(--z-base);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* 玻璃拟态工具类(DESIGN.md §4) */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
backdrop-filter: blur(16px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(140%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Toast notification */
|
||||
.landing-toast {
|
||||
position: fixed;
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: var(--z-toast);
|
||||
padding: 10px 24px;
|
||||
background: var(--color-text-primary);
|
||||
color: #FFFFFF;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideDown var(--transition-normal);
|
||||
white-space: nowrap;
|
||||
/* ===== 顶栏 ===== */
|
||||
.lp-header {
|
||||
position: sticky; top: 0; z-index: var(--z-sticky);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
.lp-header-inner {
|
||||
max-width: 1200px; margin: 0 auto; padding: 0 var(--space-2xl);
|
||||
height: 64px; display: flex; align-items: center; gap: var(--space-2xl);
|
||||
}
|
||||
.lp-brand { display: flex; align-items: center; gap: var(--space-sm); }
|
||||
.brand-name { font-weight: 700; font-size: var(--text-lg); letter-spacing: .2px; }
|
||||
.lp-nav { display: flex; gap: var(--space-xl); flex: 1; }
|
||||
.lp-nav a {
|
||||
color: var(--color-text-secondary); text-decoration: none; font-size: var(--text-base);
|
||||
font-weight: 500; cursor: pointer; transition: color var(--transition-fast);
|
||||
}
|
||||
.lp-nav a:hover { color: var(--color-accent); }
|
||||
.lp-header-actions { display: flex; gap: var(--space-sm); align-items: center; }
|
||||
|
||||
.landing-section-inner {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-2xl);
|
||||
/* 按钮(DESIGN.md §4) */
|
||||
.lp-btn {
|
||||
font-family: var(--font-sans); font-size: var(--text-base); font-weight: 600;
|
||||
border-radius: var(--radius-md); padding: 9px 18px; cursor: pointer;
|
||||
transition: all var(--transition-normal); border: 1px solid transparent; white-space: nowrap;
|
||||
}
|
||||
.lp-btn-lg { padding: 13px 26px; font-size: var(--text-md); }
|
||||
.lp-btn-primary { background: var(--color-accent); color: #fff; }
|
||||
.lp-btn-primary:hover { background: var(--color-accent-hover); transform: translateY(-1px); box-shadow: var(--shadow-md); }
|
||||
.lp-btn-outline { background: transparent; color: var(--color-accent); border-color: var(--color-accent); }
|
||||
.lp-btn-outline:hover { background: var(--color-accent-subtle); transform: translateY(-1px); }
|
||||
.lp-btn-ghost { background: transparent; color: var(--color-text-secondary); }
|
||||
.lp-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
|
||||
|
||||
/* ===== Header ===== */
|
||||
.landing-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
.lp-menu-toggle { display: none; background: transparent; border: none; color: var(--color-text-primary); cursor: pointer; margin-left: auto; }
|
||||
.lp-mobile-menu {
|
||||
display: flex; flex-direction: column; gap: var(--space-xs);
|
||||
padding: var(--space-md) var(--space-2xl) var(--space-2xl);
|
||||
border-top: 1px solid var(--color-border-light); background: var(--color-bg-secondary);
|
||||
}
|
||||
.lp-mobile-menu a { color: var(--color-text-secondary); text-decoration: none; padding: 12px 0; font-weight: 500; cursor: pointer; }
|
||||
.lp-mobile-actions { display: flex; gap: var(--space-sm); margin-top: var(--space-sm); }
|
||||
.lp-mobile-actions .lp-btn { flex: 1; }
|
||||
|
||||
.landing-header-inner {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-2xl);
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2xl);
|
||||
}
|
||||
|
||||
.landing-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.landing-nav-links {
|
||||
display: flex;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.landing-nav-links a {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.landing-nav-links a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.landing-header-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
.landing-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: 8px 20px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all var(--transition-fast);
|
||||
font-family: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.landing-btn-primary {
|
||||
background: var(--color-accent);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.landing-btn-primary:hover {
|
||||
background: var(--color-accent-hover);
|
||||
box-shadow: 0 2px 12px rgba(91, 95, 237, 0.35);
|
||||
}
|
||||
|
||||
.landing-btn-outline {
|
||||
background: transparent;
|
||||
color: var(--color-accent);
|
||||
border: 1.5px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.landing-btn-outline:hover {
|
||||
background: var(--color-accent-subtle);
|
||||
}
|
||||
|
||||
.landing-btn-lg {
|
||||
padding: 12px 28px;
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
/* ===== 通用容器 / 标题 ===== */
|
||||
.lp-inner { max-width: 1200px; margin: 0 auto; padding: 0 var(--space-2xl); }
|
||||
.lp-section { padding: var(--space-5xl) 0; }
|
||||
/* 锚点跳转时避开 sticky 顶栏遮挡 */
|
||||
.lp-section[id] { scroll-margin-top: 76px; }
|
||||
.lp-section-alt { background: var(--color-bg-tertiary); }
|
||||
.lp-section-head { text-align: center; max-width: 720px; margin: 0 auto var(--space-4xl); }
|
||||
.lp-h2 { font-size: var(--text-2xl); font-weight: 700; letter-spacing: -.2px; }
|
||||
.lp-sub { color: var(--color-text-secondary); font-size: var(--text-md); margin-top: var(--space-sm); }
|
||||
|
||||
/* ===== Hero ===== */
|
||||
.landing-hero {
|
||||
padding: 80px 0 72px;
|
||||
background: linear-gradient(180deg, #F8F7FF 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
.landing-hero-content {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-2xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 48px;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.03em;
|
||||
margin: 0 0 var(--space-xl);
|
||||
}
|
||||
|
||||
.hero-accent {
|
||||
background: linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 640px;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 var(--space-2xl);
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-3xl);
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
gap: var(--space-3xl);
|
||||
}
|
||||
|
||||
.hero-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== Carousel ===== */
|
||||
.landing-banners {
|
||||
padding: var(--space-3xl) 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-xl);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text-tertiary);
|
||||
margin: var(--space-sm) 0 0;
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
position: relative;
|
||||
.lp-hero {
|
||||
background: linear-gradient(180deg, #EEEDFF 0%, #F8F9FB 42%);
|
||||
padding: var(--space-5xl) 0 var(--space-4xl);
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.carousel-track {
|
||||
display: flex;
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.lp-hero-grid {
|
||||
max-width: 1200px; margin: 0 auto; padding: 0 var(--space-2xl);
|
||||
display: grid; grid-template-columns: 1.05fr .95fr; gap: var(--space-4xl); align-items: center;
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
.lp-eyebrow {
|
||||
display: inline-block; font-size: var(--text-sm); font-weight: 600; color: var(--color-accent);
|
||||
background: var(--color-accent-light); padding: 4px 12px; border-radius: var(--radius-full); margin-bottom: var(--space-lg);
|
||||
}
|
||||
.lp-title { font-size: var(--text-3xl); font-weight: 800; line-height: 1.25; letter-spacing: -.5px; }
|
||||
.lp-accent { color: var(--color-accent); }
|
||||
.lp-desc { color: var(--color-text-secondary); font-size: var(--text-md); line-height: 1.7; margin: var(--space-lg) 0 var(--space-2xl); max-width: 520px; }
|
||||
.kw { color: var(--color-accent); font-weight: 600; }
|
||||
.lp-actions { display: flex; gap: var(--space-md); flex-wrap: wrap; }
|
||||
.lp-stats { display: flex; gap: var(--space-3xl); margin-top: var(--space-3xl); }
|
||||
.lp-stats > div { display: flex; flex-direction: column; }
|
||||
.lp-stat-num { font-size: var(--text-xl); font-weight: 800; color: var(--color-text-primary); }
|
||||
.lp-stat-label { font-size: var(--text-sm); color: var(--color-text-tertiary); margin-top: 2px; }
|
||||
|
||||
.carousel-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-3xl) var(--space-3xl);
|
||||
border-radius: var(--radius-xl);
|
||||
min-height: 200px;
|
||||
color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.lp-hero-visual { position: relative; min-height: 320px; display: flex; align-items: center; justify-content: center; }
|
||||
.lp-hero-glow {
|
||||
position: absolute; width: 320px; height: 320px; border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(91,95,237,.22), transparent 70%);
|
||||
filter: blur(8px);
|
||||
}
|
||||
.lp-hero-card { position: relative; width: 100%; max-width: 380px; border-radius: var(--radius-xl); padding: var(--space-2xl); display: flex; flex-direction: column; gap: var(--space-md); }
|
||||
.lp-hc-row { display: flex; align-items: center; gap: var(--space-sm); font-size: var(--text-sm); color: var(--color-text-secondary); font-weight: 500; }
|
||||
.lp-hc-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-success); box-shadow: 0 0 0 3px var(--color-success-light); }
|
||||
.lp-hc-bubble { background: var(--color-bg-tertiary); border-radius: var(--radius-lg); padding: var(--space-md); font-size: var(--text-sm); color: var(--color-text-primary); }
|
||||
.lp-hc-bubble-ai { background: var(--color-accent-light); color: var(--color-accent); font-weight: 500; }
|
||||
.lp-hc-bar { height: 6px; border-radius: var(--radius-full); background: var(--color-bg-tertiary); overflow: hidden; }
|
||||
.lp-hc-bar > span { display: block; height: 100%; width: 70%; border-radius: var(--radius-full); background: linear-gradient(90deg, #5B5FED, #4F8DF9); }
|
||||
|
||||
.carousel-card-content {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.carousel-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-md);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.carousel-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 var(--space-sm);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.carousel-subtitle {
|
||||
font-size: var(--text-base);
|
||||
opacity: 0.9;
|
||||
margin: 0 0 var(--space-xl);
|
||||
}
|
||||
|
||||
.carousel-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: 8px 20px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.4);
|
||||
border-radius: var(--radius-md);
|
||||
color: #FFFFFF;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.carousel-cta:hover {
|
||||
background: rgba(255,255,255,0.35);
|
||||
border-color: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
.carousel-card-visual {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.visual-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.visual-circle.c1 {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.visual-circle.c2 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
top: 50px;
|
||||
right: 50px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.visual-circle.c3 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
/* Dots */
|
||||
.carousel-dots {
|
||||
position: absolute;
|
||||
bottom: var(--space-lg);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.carousel-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255,255,255,0.4);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.carousel-dot.active {
|
||||
background: #FFFFFF;
|
||||
width: 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Arrows */
|
||||
.carousel-arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255,255,255,0.9);
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all var(--transition-fast);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.carousel-arrow:hover {
|
||||
background: #FFFFFF;
|
||||
/* ===== 轮播广告位 ===== */
|
||||
.lp-band { padding: var(--space-4xl) 0; }
|
||||
.lp-carousel { position: relative; }
|
||||
.lp-track { display: flex; transition: transform var(--transition-slow); }
|
||||
.lp-slide { min-width: 100%; padding: 0 var(--space-xs); }
|
||||
.lp-carousel-card {
|
||||
border-radius: var(--radius-xl); padding: var(--space-4xl); color: #fff; position: relative; overflow: hidden;
|
||||
display: flex; justify-content: space-between; align-items: center; min-height: 200px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.carousel-arrow-left {
|
||||
left: var(--space-md);
|
||||
.lp-cc-content { position: relative; z-index: 2; max-width: 60%; }
|
||||
.lp-cc-badge { display: inline-block; background: rgba(255,255,255,.22); padding: 4px 12px; border-radius: var(--radius-full); font-size: var(--text-xs); font-weight: 600; margin-bottom: var(--space-md); }
|
||||
.lp-cc-title { font-size: var(--text-xl); font-weight: 700; }
|
||||
.lp-cc-sub { font-size: var(--text-sm); opacity: .9; margin-top: var(--space-xs); }
|
||||
.lp-cc-cta {
|
||||
margin-top: var(--space-lg); display: inline-flex; align-items: center; gap: 6px;
|
||||
background: #fff; color: var(--color-accent); border: none; border-radius: var(--radius-md);
|
||||
padding: 9px 18px; font-weight: 600; font-size: var(--text-sm); cursor: pointer; transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.carousel-arrow-right {
|
||||
right: var(--space-md);
|
||||
.lp-cc-cta:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,.18); }
|
||||
.lp-cc-visual { position: relative; width: 120px; height: 120px; }
|
||||
.lp-cc-visual .c1, .lp-cc-visual .c2, .lp-cc-visual .c3 {
|
||||
position: absolute; border-radius: 50%; background: rgba(255,255,255,.25);
|
||||
}
|
||||
.lp-cc-visual .c1 { width: 70px; height: 70px; top: 0; right: 0; }
|
||||
.lp-cc-visual .c2 { width: 48px; height: 48px; bottom: 6px; right: 30px; }
|
||||
.lp-cc-visual .c3 { width: 30px; height: 30px; bottom: 0; left: 0; }
|
||||
|
||||
/* ===== Market ===== */
|
||||
.landing-market {
|
||||
padding: var(--space-3xl) 0 var(--space-4xl);
|
||||
background: var(--color-bg-secondary);
|
||||
.lp-arrow {
|
||||
position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
|
||||
width: 44px; height: 44px; border-radius: 50%; border: none; cursor: pointer;
|
||||
background: rgba(255,255,255,.85); color: var(--color-accent); display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: var(--shadow-md); transition: all var(--transition-normal);
|
||||
}
|
||||
.lp-arrow:hover { background: #fff; transform: translateY(-50%) scale(1.05); }
|
||||
.lp-arrow-l { left: 12px; } .lp-arrow-r { right: 12px; }
|
||||
.lp-dots { display: flex; justify-content: center; gap: var(--space-sm); margin-top: var(--space-lg); }
|
||||
.lp-dot { width: 8px; height: 8px; border-radius: 50%; border: none; background: var(--color-border-hover); cursor: pointer; transition: all var(--transition-fast); }
|
||||
.lp-dot.on { width: 24px; border-radius: var(--radius-full); background: var(--color-accent); }
|
||||
|
||||
.market-filter-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-2xl);
|
||||
}
|
||||
/* ===== 栅格 ===== */
|
||||
.lp-grid { display: grid; gap: var(--space-2xl); }
|
||||
.lp-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.lp-grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
.lp-grid-5 { grid-template-columns: repeat(5, 1fr); }
|
||||
|
||||
.filter-tab {
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--color-border);
|
||||
background: #FFFFFF;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
font-family: inherit;
|
||||
}
|
||||
/* 痛点卡 */
|
||||
.lp-pain { background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-2xl); transition: all var(--transition-normal); }
|
||||
.lp-pain:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); border-color: var(--color-border-hover); }
|
||||
.lp-pain-icon { width: 48px; height: 48px; border-radius: var(--radius-md); background: var(--color-accent-light); color: var(--color-accent); display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-lg); }
|
||||
.lp-pain-title { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-sm); }
|
||||
.lp-pain-desc { color: var(--color-text-secondary); font-size: var(--text-sm); line-height: 1.65; }
|
||||
|
||||
.filter-tab:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
/* 价值卡 */
|
||||
.lp-value { background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-2xl); transition: all var(--transition-normal); }
|
||||
.lp-value:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
|
||||
.lp-value-icon { width: 48px; height: 48px; border-radius: var(--radius-md); background: var(--gradient-brand, #EEEDFF); color: var(--color-accent); display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-lg); }
|
||||
.lp-value-title { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-sm); }
|
||||
.lp-value-desc { color: var(--color-text-secondary); font-size: var(--text-sm); line-height: 1.65; }
|
||||
|
||||
.filter-tab.active {
|
||||
background: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
/* 能力卡 */
|
||||
.lp-feature { background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-2xl); transition: all var(--transition-normal); }
|
||||
.lp-feature:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
|
||||
.lp-feature-icon { width: 52px; height: 52px; border-radius: var(--radius-md); background: var(--color-accent-light); color: var(--color-accent); display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-lg); }
|
||||
.lp-feature-title { font-size: var(--text-lg); font-weight: 600; margin-bottom: var(--space-sm); }
|
||||
.lp-feature-desc { color: var(--color-text-secondary); font-size: var(--text-sm); line-height: 1.65; }
|
||||
|
||||
.market-app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: var(--space-xl);
|
||||
/* ===== 热门技能榜 ===== */
|
||||
.lp-rank { list-style: none; display: flex; flex-direction: column; gap: var(--space-md); counter-reset: rank; }
|
||||
.lp-rank-item {
|
||||
display: grid; grid-template-columns: 44px 64px 1fr auto; gap: var(--space-lg); align-items: center;
|
||||
background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-lg);
|
||||
padding: var(--space-lg) var(--space-xl); cursor: pointer; transition: all var(--transition-normal);
|
||||
}
|
||||
.lp-rank-item:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); border-color: var(--color-accent-light); }
|
||||
.lp-rank-no { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; background: var(--color-bg-tertiary); color: var(--color-text-secondary); font-weight: 700; font-size: var(--text-md); }
|
||||
.lp-rank-no.top { background: linear-gradient(135deg, #5B5FED, #4F8DF9); color: #fff; box-shadow: var(--shadow-md); }
|
||||
.lp-rank-cover { position: relative; width: 64px; height: 64px; border-radius: var(--radius-md); overflow: hidden; }
|
||||
.lp-rank-badge { position: absolute; top: 6px; left: 6px; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: var(--radius-full); color: #fff; }
|
||||
.badge-skill { background: var(--color-success); }
|
||||
.badge-agent { background: var(--color-accent); }
|
||||
.badge-mcp { background: var(--color-warning); }
|
||||
.lp-rank-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.lp-rank-name { font-weight: 600; font-size: var(--text-md); }
|
||||
.lp-rank-desc { color: var(--color-text-tertiary); font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lp-rank-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; color: var(--color-text-secondary); font-size: var(--text-sm); }
|
||||
.lp-rank-installs { display: inline-flex; align-items: center; gap: 4px; color: var(--color-accent); font-weight: 600; }
|
||||
.lp-rank-rating { color: var(--color-warning); font-weight: 600; }
|
||||
|
||||
.market-app-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.market-app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--color-accent-light);
|
||||
}
|
||||
|
||||
.app-card-cover {
|
||||
height: 120px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding: var(--space-md);
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.app-card-type-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.app-card-price {
|
||||
position: absolute;
|
||||
top: var(--space-md);
|
||||
left: var(--space-md);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 2px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.app-card-body {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.app-card-name {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-xs);
|
||||
}
|
||||
|
||||
.app-card-desc {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 var(--space-md);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.app-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.app-card-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: #F59E0B;
|
||||
}
|
||||
|
||||
.app-card-rating svg {
|
||||
fill: #F59E0B;
|
||||
}
|
||||
|
||||
.market-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-2xl);
|
||||
}
|
||||
|
||||
/* ===== Features ===== */
|
||||
.landing-features {
|
||||
padding: var(--space-4xl) 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: var(--space-2xl);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--color-accent-light);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-sm);
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
/* ===== 应用市场网格 ===== */
|
||||
.lp-app-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2xl); }
|
||||
.lp-app-card { background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow: hidden; cursor: pointer; transition: all var(--transition-normal); }
|
||||
.lp-app-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
|
||||
.lp-app-cover { position: relative; height: 120px; }
|
||||
.lp-app-badge { position: absolute; top: 10px; left: 10px; font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: var(--radius-full); color: #fff; }
|
||||
.lp-app-price { position: absolute; top: 10px; right: 10px; font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: var(--radius-full); background: rgba(255,255,255,.9); color: var(--color-text-primary); }
|
||||
.lp-app-body { padding: var(--space-lg); }
|
||||
.lp-app-name { font-size: var(--text-md); font-weight: 600; margin-bottom: 6px; }
|
||||
.lp-app-desc { color: var(--color-text-tertiary); font-size: var(--text-sm); line-height: 1.6; height: 42px; overflow: hidden; }
|
||||
.lp-app-meta { display: flex; justify-content: space-between; margin-top: var(--space-md); font-size: var(--text-xs); color: var(--color-text-secondary); }
|
||||
.lp-app-meta span { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.lp-more { text-align: center; margin-top: var(--space-3xl); }
|
||||
|
||||
/* ===== CTA ===== */
|
||||
.landing-cta {
|
||||
padding: var(--space-4xl) 0;
|
||||
background: linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
margin: 0 0 var(--space-md);
|
||||
}
|
||||
|
||||
.cta-desc {
|
||||
font-size: var(--text-lg);
|
||||
color: rgba(255,255,255,0.85);
|
||||
margin: 0 0 var(--space-2xl);
|
||||
}
|
||||
|
||||
.landing-cta .landing-btn-primary {
|
||||
background: #FFFFFF;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.landing-cta .landing-btn-primary:hover {
|
||||
background: #F0EEFF;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
}
|
||||
.lp-cta { padding: var(--space-5xl) 0; background: linear-gradient(135deg, #5B5FED 0%, #4F8DF9 100%); }
|
||||
.lp-cta .lp-inner { text-align: center; color: #fff; }
|
||||
.lp-cta-title { font-size: var(--text-2xl); font-weight: 800; }
|
||||
.lp-cta-desc { opacity: .92; margin: var(--space-md) 0 var(--space-2xl); font-size: var(--text-md); }
|
||||
.lp-cta .lp-btn-primary { background: #fff; color: var(--color-accent); }
|
||||
.lp-cta .lp-btn-primary:hover { background: #f0f0ff; }
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.landing-footer {
|
||||
padding: var(--space-3xl) 0 var(--space-xl);
|
||||
background: var(--color-bg-tertiary);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.lp-footer { background: #fff; border-top: 1px solid var(--color-border); padding: var(--space-4xl) 0 var(--space-2xl); }
|
||||
.lp-footer-grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: var(--space-2xl); }
|
||||
.lp-footer-brand p { color: var(--color-text-tertiary); font-size: var(--text-sm); margin-top: var(--space-md); max-width: 280px; line-height: 1.6; }
|
||||
.lp-footer-col h4 { font-size: var(--text-sm); font-weight: 700; margin-bottom: var(--space-md); }
|
||||
.lp-footer-col a, .lp-footer-col span { display: block; color: var(--color-text-secondary); font-size: var(--text-sm); margin-bottom: var(--space-sm); cursor: pointer; text-decoration: none; transition: color var(--transition-fast); }
|
||||
.lp-footer-col a:hover { color: var(--color-accent); }
|
||||
.lp-footer-bottom { margin-top: var(--space-3xl); padding-top: var(--space-lg); border-top: 1px solid var(--color-border-light); color: var(--color-text-tertiary); font-size: var(--text-xs); text-align: center; }
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2xl);
|
||||
margin-bottom: var(--space-2xl);
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width: 1024px) {
|
||||
.lp-hero-grid { grid-template-columns: 1fr; }
|
||||
.lp-hero-visual { min-height: 260px; margin-top: var(--space-2xl); }
|
||||
.lp-grid-5 { grid-template-columns: repeat(3, 1fr); }
|
||||
.lp-grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||
.lp-app-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: var(--space-4xl);
|
||||
}
|
||||
|
||||
.footer-col h4 {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 var(--space-md);
|
||||
}
|
||||
|
||||
.footer-col a {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: var(--space-sm);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.footer-col a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
padding-top: var(--space-xl);
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 768px) {
|
||||
.landing-nav-links {
|
||||
display: none;
|
||||
}
|
||||
.lp-nav, .lp-header-actions { display: none; }
|
||||
.lp-menu-toggle { display: inline-flex; }
|
||||
.lp-section { padding: var(--space-3xl) 0; }
|
||||
.lp-grid-5, .lp-grid-4, .lp-grid-3 { grid-template-columns: repeat(2, 1fr); }
|
||||
.lp-app-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.lp-footer-grid { grid-template-columns: 1fr 1fr; }
|
||||
.lp-stats { gap: var(--space-2xl); }
|
||||
.lp-rank-item { grid-template-columns: 36px 52px 1fr; }
|
||||
.lp-rank-meta { display: none; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.lp-inner, .lp-header-inner, .lp-hero-grid { padding-left: var(--space-lg); padding-right: var(--space-lg); }
|
||||
.lp-grid-5, .lp-grid-4, .lp-grid-3, .lp-app-grid { grid-template-columns: 1fr; }
|
||||
.lp-footer-grid { grid-template-columns: 1fr; }
|
||||
.lp-stats { flex-wrap: wrap; gap: var(--space-lg); }
|
||||
.lp-title { font-size: var(--text-2xl); }
|
||||
.lp-cc-content { max-width: 100%; }
|
||||
.lp-cc-visual { display: none; }
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.carousel-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.carousel-card-visual {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.market-app-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
/* 尊重减弱动效偏好 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { transition: none !important; animation: none !important; }
|
||||
}
|
||||
@@ -4,39 +4,85 @@ import { mockAdBanners, mockApps } from '@/mock/data';
|
||||
import type { AdBanner, MarketApp } from '@/types';
|
||||
import './LandingPage.css';
|
||||
|
||||
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>
|
||||
/* ---------- 内联 Lucide 风格图标(满足"用 Lucide 图标、禁 emoji"铁律) ---------- */
|
||||
const Icon: React.FC<{ d: React.ReactNode; size?: number }> = ({ d, size = 20 }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">{d}</svg>
|
||||
);
|
||||
|
||||
/* ---------- 品牌同系渐变(仅靛蓝→青蓝,无第三色相) ---------- */
|
||||
const appGradients: Record<string, string> = {
|
||||
a1: 'linear-gradient(135deg,#5B5FED 0%,#4F8DF9 100%)',
|
||||
a2: 'linear-gradient(135deg,#6366F1 0%,#60A5FA 100%)',
|
||||
a3: 'linear-gradient(135deg,#5B5FED 0%,#818CF8 100%)',
|
||||
a4: 'linear-gradient(135deg,#4F8DF9 0%,#38BDF8 100%)',
|
||||
a5: 'linear-gradient(135deg,#6366F1 0%,#7DD3FC 100%)',
|
||||
a6: 'linear-gradient(135deg,#5B5FED 0%,#A78BFA 100%)',
|
||||
a7: 'linear-gradient(135deg,#4F8DF9 0%,#818CF8 100%)',
|
||||
a8: 'linear-gradient(135deg,#6366F1 0%,#4F8DF9 100%)',
|
||||
};
|
||||
const gradFor = (id: string) => appGradients[id] || 'linear-gradient(135deg,#5B5FED 0%,#4F8DF9 100%)';
|
||||
|
||||
const typeLabels: Record<string, string> = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' };
|
||||
const typeColors: Record<string, string> = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' };
|
||||
const typeBadge: Record<string, string> = {
|
||||
skill: 'badge-skill', agent: 'badge-agent', mcp: 'badge-mcp',
|
||||
};
|
||||
|
||||
/* ---------- 文案数据(来自用户原始需求提炼) ---------- */
|
||||
const painPoints = [
|
||||
{ d: <><path d="M12 1v22" /><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" /></>,
|
||||
title: '大模型 API 贵且复杂', desc: '多家模型计费规则不一、调用门槛高,个人与小企业难以低成本接入。' },
|
||||
{ d: <><path d="M12 20h9" /><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" /></>,
|
||||
title: 'Prompt 难调试', desc: '缺乏可视化编排与复用,效果全靠手感,团队无法沉淀最佳实践。' },
|
||||
{ d: <><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5" /><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3" /></>,
|
||||
title: '数据孤岛', desc: '知识散落在文档、表格、聊天记录里,AI 答非所问,价值无法被激活。' },
|
||||
{ d: <><path d="M3 3v18h18" /><path d="m19 9-5 5-4-4-3 3" /></>,
|
||||
title: '个人开发者无变现渠道', desc: '做出好用的 AI 能力却找不到分发与交易场景,难以转化为收入。' },
|
||||
];
|
||||
|
||||
const valueProps = [
|
||||
{ d: <><path d="M12 1v22" /><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" /></>,
|
||||
title: '低价订阅 · 无限 Token', kw: '无限Token',
|
||||
desc: '一个低价会员即可放开使用,告别按次计费与额度焦虑,放心把 AI 用进日常。' },
|
||||
{ d: <><rect x="3" y="3" width="7" height="7" rx="1.2" /><rect x="14" y="3" width="7" height="7" rx="1.2" /><rect x="3" y="14" width="7" height="7" rx="1.2" /><rect x="14" y="14" width="7" height="7" rx="1.2" /></>,
|
||||
title: '垂直可用办公模板', kw: '办公提效',
|
||||
desc: '写 PPT、出投标书、做运营、排版公众号——开箱即用的模板,把重复劳动交给 AI。' },
|
||||
{ d: <><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><path d="M3 6h18" /><path d="M16 10a4 4 0 0 1-8 0" /></>,
|
||||
title: '可交易的技能市场', kw: 'AI变现',
|
||||
desc: '把自己打磨的技能上架交易,按订阅 / 下载获得收益,能力即资产。' },
|
||||
{ d: <><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" /></>,
|
||||
title: 'AI 创作试炼场', kw: '',
|
||||
desc: '写小说、做漫剧、谱音乐——低成本试错,让创作想法快速跑通。' },
|
||||
{ d: <><path d="M3 12a9 9 0 1 0 9-9" /><path d="M3 3v6h6" /><path d="M12 7v5l3 3" /></>,
|
||||
title: '低成本养龙虾', kw: '',
|
||||
desc: '低成本微调 / 托管你的专属模型,把"养模型"变成可负担的日常。' },
|
||||
];
|
||||
|
||||
const features = [
|
||||
{ d: <><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></>,
|
||||
title: '多模型 AI 对话', desc: 'DeepSeek / Qwen / 国际模型自由切换,按需选用最优模型。' },
|
||||
{ d: <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" /><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" /></>,
|
||||
title: '私有知识库', desc: '上传文档构建专属知识库,AI 回答基于你的内容,更精准可靠。' },
|
||||
{ d: <><rect x="3" y="3" width="7" height="7" rx="1.2" /><rect x="14" y="3" width="7" height="7" rx="1.2" /><rect x="3" y="14" width="7" height="7" rx="1.2" /><rect x="14" y="14" width="7" height="7" rx="1.2" /></>,
|
||||
title: '技能 / 应用市场', desc: '丰富的 Skills、Agent、MCP 应用,即装即用,一键接入工作流。' },
|
||||
{ d: <><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></>,
|
||||
title: 'Token 驾驶舱', desc: '实时监控 Token 消耗,费用透明可控,每一笔都有据可查。' },
|
||||
{ d: <><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></>,
|
||||
title: 'IM 频道绑定', desc: '微信 / 企微 / 飞书 / QQ 多平台绑定,AI 助手融入日常工作流。' },
|
||||
{ d: <><polyline points="16 3 21 3 21 8" /><line x1="4" y1="20" x2="21" y2="3" /><polyline points="21 16 21 21 16 21" /><line x1="15" y1="15" x2="21" y2="21" /><line x1="4" y1="4" x2="9" y2="9" /></>,
|
||||
title: '自动化任务编排', desc: '关键词触发、定时执行等自动化任务,让 AI 按你的节奏工作。' },
|
||||
];
|
||||
|
||||
const LandingPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [mobileMenu, setMobileMenu] = useState(false);
|
||||
|
||||
const banners = mockAdBanners.filter(b => b.status === 'active');
|
||||
const publishedApps = mockApps.filter(a => a.status === 'published');
|
||||
const [appTypeFilter, setAppTypeFilter] = useState<'' | 'skill' | 'agent' | 'mcp'>('');
|
||||
const [toastMsg, setToastMsg] = useState('');
|
||||
const topSkills = [...publishedApps].sort((a, b) => b.installCount - a.installCount).slice(0, 5);
|
||||
|
||||
const filteredApps = appTypeFilter
|
||||
? publishedApps.filter(a => a.type === appTypeFilter)
|
||||
: publishedApps;
|
||||
|
||||
const showToast = (msg: string) => {
|
||||
setToastMsg(msg);
|
||||
setTimeout(() => setToastMsg(''), 2000);
|
||||
};
|
||||
|
||||
const handleAppClick = (appName: string) => {
|
||||
showToast(`"${appName}" 需登录后使用,即将跳转登录页`);
|
||||
setTimeout(() => navigate('/login'), 800);
|
||||
};
|
||||
|
||||
// Auto rotate carousel
|
||||
const nextSlide = useCallback(() => {
|
||||
if (banners.length === 0) return;
|
||||
setCurrentSlide(prev => (prev + 1) % banners.length);
|
||||
@@ -44,323 +90,315 @@ const LandingPage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (banners.length <= 1 || isPaused) return;
|
||||
const timer = setInterval(nextSlide, 4000);
|
||||
const timer = setInterval(nextSlide, 4500);
|
||||
return () => clearInterval(timer);
|
||||
}, [banners.length, isPaused, nextSlide]);
|
||||
|
||||
const goToSlide = (idx: number) => setCurrentSlide(idx);
|
||||
// SEO / GEO:动态写入标题与描述
|
||||
useEffect(() => {
|
||||
document.title = '七虎AI · 个人体验 AI、办公提效与 AI 创收的一站式 PaaS 平台';
|
||||
let meta = document.querySelector('meta[name="description"]');
|
||||
if (!meta) { meta = document.createElement('meta'); meta.setAttribute('name', 'description'); document.head.appendChild(meta); }
|
||||
meta.setAttribute('content', '七虎AI 是面向个人与职场用户的 AI PaaS 平台:低价订阅无限 Token、垂直办公模板、可交易技能市场、AI 创作试炼场与低成本养模型,让每个人用 AI 把事做成。');
|
||||
}, []);
|
||||
|
||||
const getGradient = (app: MarketApp): string => {
|
||||
const gradients: Record<string, string> = {
|
||||
a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||
a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
||||
a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)',
|
||||
a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
|
||||
};
|
||||
return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)';
|
||||
const goLogin = () => navigate('/login');
|
||||
|
||||
// 点击 logo 回到首页顶部(.lp 是滚动容器,用 scrollTo 而非 window)
|
||||
const goTop = () => {
|
||||
const el = document.querySelector('.lp') as HTMLElement | null;
|
||||
if (el) el.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
else window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
// HashRouter 下原生 <a href="#id"> 会被当成路由路径解析,导致菜单失效;
|
||||
// 故改为编程式滚动 + preventDefault,仅在 LandingPage 内部滚动容器内跳转。
|
||||
const anchorClick = (id: string, after?: () => void) => (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
after?.();
|
||||
};
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: '七虎AI',
|
||||
applicationCategory: 'BusinessApplication',
|
||||
operatingSystem: 'Web',
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' },
|
||||
aggregateRating: { '@type': 'AggregateRating', ratingValue: '4.8', reviewCount: '1000' },
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="landing-page">
|
||||
{/* Toast notification */}
|
||||
{toastMsg && (
|
||||
<div className="landing-toast">{toastMsg}</div>
|
||||
)}
|
||||
<div className="lp">
|
||||
{/* JSON-LD 富媒体摘要(SEO / GEO) */}
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||
|
||||
{/* Hero / Nav bar */}
|
||||
<header className="landing-header">
|
||||
<div className="landing-header-inner">
|
||||
<div className="landing-brand">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
|
||||
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
|
||||
{/* 浮层玻璃顶栏 */}
|
||||
<header className="lp-header glass">
|
||||
<div className="lp-header-inner">
|
||||
<div className="lp-brand" onClick={goTop} role="button" tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && goTop()} aria-label="回到顶部">
|
||||
<svg width="30" height="30" viewBox="0 0 32 32" fill="none" aria-hidden>
|
||||
<rect width="32" height="32" rx="8" fill="#5B5FED" />
|
||||
<path d="M8 20V12l4 4 4-4v8" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M6 22h20" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" opacity="0.5" />
|
||||
</svg>
|
||||
<span className="brand-name">Qihu-AIFlow</span>
|
||||
<span className="brand-name">七虎AI</span>
|
||||
</div>
|
||||
<nav className="landing-nav-links">
|
||||
<a href="#market">应用市场</a>
|
||||
<a href="#features">平台能力</a>
|
||||
|
||||
<nav className="lp-nav" aria-label="主导航">
|
||||
<a href="#positioning" onClick={anchorClick('positioning')}>平台定位</a>
|
||||
<a href="#value" onClick={anchorClick('value')}>平台价值</a>
|
||||
<a href="#top" onClick={anchorClick('top')}>热门技能</a>
|
||||
<a href="#market" onClick={anchorClick('market')}>应用市场</a>
|
||||
<a href="#features" onClick={anchorClick('features')}>平台能力</a>
|
||||
</nav>
|
||||
<div className="landing-header-actions">
|
||||
<button className="landing-btn landing-btn-outline" onClick={() => navigate('/login')}>登录</button>
|
||||
<button className="landing-btn landing-btn-primary" onClick={() => navigate('/login')}>免费注册</button>
|
||||
|
||||
<div className="lp-header-actions">
|
||||
<button className="lp-btn lp-btn-ghost" onClick={goLogin}>登录</button>
|
||||
<button className="lp-btn lp-btn-primary" onClick={goLogin}>免费注册</button>
|
||||
</div>
|
||||
|
||||
<button className="lp-menu-toggle" aria-label="菜单" onClick={() => setMobileMenu(v => !v)}>
|
||||
<Icon d={mobileMenu ? <><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></> : <><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></>} size={22} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mobileMenu && (
|
||||
<div className="lp-mobile-menu">
|
||||
<a href="#positioning" onClick={anchorClick('positioning', () => setMobileMenu(false))}>平台定位</a>
|
||||
<a href="#value" onClick={anchorClick('value', () => setMobileMenu(false))}>平台价值</a>
|
||||
<a href="#top" onClick={anchorClick('top', () => setMobileMenu(false))}>热门技能</a>
|
||||
<a href="#market" onClick={anchorClick('market', () => setMobileMenu(false))}>应用市场</a>
|
||||
<a href="#features" onClick={anchorClick('features', () => setMobileMenu(false))}>平台能力</a>
|
||||
<div className="lp-mobile-actions">
|
||||
<button className="lp-btn lp-btn-ghost" onClick={() => { setMobileMenu(false); goLogin(); }}>登录</button>
|
||||
<button className="lp-btn lp-btn-primary" onClick={() => { setMobileMenu(false); goLogin(); }}>免费注册</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="landing-hero">
|
||||
<div className="landing-hero-content">
|
||||
<h1 className="hero-title">
|
||||
用 <span className="hero-accent">AI</span> 构建你的<br/>智能自动化工作流
|
||||
</h1>
|
||||
<p className="hero-desc">
|
||||
Qihu-AIFlow 是一站式AI智能自动化PaaS平台,集成多模态大模型、知识库管理、应用市场,
|
||||
让每个人都能轻松搭建属于自己的AI自动化SOP。
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<button className="landing-btn landing-btn-primary landing-btn-lg" onClick={() => navigate('/login')}>
|
||||
免费开始使用
|
||||
</button>
|
||||
<button className="landing-btn landing-btn-outline landing-btn-lg" onClick={() => { document.getElementById('market')?.scrollIntoView({ behavior: 'smooth' }); }}>
|
||||
浏览应用市场
|
||||
</button>
|
||||
</div>
|
||||
<div className="hero-stats">
|
||||
<div className="hero-stat">
|
||||
<span className="stat-number">3,500+</span>
|
||||
<span className="stat-label">活跃用户</span>
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="lp-hero">
|
||||
<div className="lp-hero-grid">
|
||||
<div className="lp-hero-text">
|
||||
<div className="lp-eyebrow">AI PaaS 平台</div>
|
||||
<h1 className="lp-title">让每个人都能<br /><span className="lp-accent">用 AI 把事做成</span></h1>
|
||||
<p className="lp-desc">
|
||||
七虎AI 是个人体验 AI、办公提效、AI 创收与自我提升的一站式交互平台。
|
||||
低价订阅 <span className="kw">无限Token</span>,提供垂直可用的办公模板、可交易的市场与低成本养模型能力。
|
||||
</p>
|
||||
<div className="lp-actions">
|
||||
<button className="lp-btn lp-btn-primary lp-btn-lg" onClick={goLogin}>免费开始使用</button>
|
||||
<button className="lp-btn lp-btn-outline lp-btn-lg" onClick={anchorClick('market')}>浏览应用市场</button>
|
||||
</div>
|
||||
<div className="lp-stats">
|
||||
<div><span className="lp-stat-num">3,500+</span><span className="lp-stat-label">活跃用户</span></div>
|
||||
<div><span className="lp-stat-num">120+</span><span className="lp-stat-label">应用 / Skills</span></div>
|
||||
<div><span className="lp-stat-num">99.8%</span><span className="lp-stat-label">服务可用率</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span className="stat-number">120+</span>
|
||||
<span className="stat-label">应用/Skills</span>
|
||||
</div>
|
||||
<div className="hero-stat">
|
||||
<span className="stat-number">99.8%</span>
|
||||
<span className="stat-label">服务可用率</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Carousel Section */}
|
||||
{banners.length > 0 && (
|
||||
<section className="landing-banners">
|
||||
<div className="landing-section-inner">
|
||||
<h2 className="section-title">推荐应用</h2>
|
||||
<div
|
||||
className="carousel-container"
|
||||
onMouseEnter={() => setIsPaused(true)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
>
|
||||
<div className="carousel-track" style={{ transform: `translateX(-${currentSlide * 100}%)` }}>
|
||||
{banners.map(banner => (
|
||||
<div key={banner.id} className="carousel-slide">
|
||||
<div className="carousel-card" style={{ background: banner.linkAppId ? getGradient({ id: banner.linkAppId } as MarketApp) : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<div className="carousel-card-content">
|
||||
<div className="carousel-badge">推荐</div>
|
||||
<h3 className="carousel-title">{banner.title}</h3>
|
||||
{banner.linkAppName && (
|
||||
<p className="carousel-subtitle">立即体验 {banner.linkAppName}</p>
|
||||
)}
|
||||
<button
|
||||
className="carousel-cta"
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
查看详情
|
||||
<SVGIcon size={14}><polyline points="9 18 15 12 9 6"/></SVGIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div className="carousel-card-visual">
|
||||
<div className="visual-circle c1"/>
|
||||
<div className="visual-circle c2"/>
|
||||
<div className="visual-circle c3"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Hero 右侧:品牌渐变光斑 + 玻璃卡视觉 */}
|
||||
<div className="lp-hero-visual" aria-hidden>
|
||||
<div className="lp-hero-glow" />
|
||||
<div className="lp-hero-card glass">
|
||||
<div className="lp-hc-row"><span className="lp-hc-dot" /> AI 对话进行中</div>
|
||||
<div className="lp-hc-bubble">帮我把这份文档整理成周报大纲</div>
|
||||
<div className="lp-hc-bubble lp-hc-bubble-ai">已生成 5 个章节,并附数据看板建议 ✓</div>
|
||||
<div className="lp-hc-bar"><span /></div>
|
||||
</div>
|
||||
{/* Dots */}
|
||||
{banners.length > 1 && (
|
||||
<div className="carousel-dots">
|
||||
{banners.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
className={`carousel-dot ${idx === currentSlide ? 'active' : ''}`}
|
||||
onClick={() => goToSlide(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Arrows */}
|
||||
{banners.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="carousel-arrow carousel-arrow-left"
|
||||
onClick={() => setCurrentSlide(prev => (prev - 1 + banners.length) % banners.length)}
|
||||
>
|
||||
<SVGIcon size={20}><polyline points="15 18 9 12 15 6"/></SVGIcon>
|
||||
</button>
|
||||
<button
|
||||
className="carousel-arrow carousel-arrow-right"
|
||||
onClick={() => nextSlide()}
|
||||
>
|
||||
<SVGIcon size={20}><polyline points="9 18 15 12 9 6"/></SVGIcon>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Market Preview Section */}
|
||||
<section className="landing-market" id="market">
|
||||
<div className="landing-section-inner">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">应用市场</h2>
|
||||
<p className="section-desc">丰富的 Skills、Agent、MCP 应用,一键接入你的工作流</p>
|
||||
{/* 轮播广告位(技能展示) */}
|
||||
{banners.length > 0 && (
|
||||
<section className="lp-band">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-section-head">
|
||||
<h2 className="lp-h2">精选推荐</h2>
|
||||
<p className="lp-sub">平台热门技能广告位,提升展现与下载</p>
|
||||
</div>
|
||||
<div className="lp-carousel" onMouseEnter={() => setIsPaused(true)} onMouseLeave={() => setIsPaused(false)}>
|
||||
<div className="lp-track" style={{ transform: `translateX(-${currentSlide * 100}%)` }}>
|
||||
{banners.map(b => (
|
||||
<div key={b.id} className="lp-slide">
|
||||
<div className="lp-carousel-card" style={{ background: gradFor(b.linkAppId || 'a1') }}>
|
||||
<div className="lp-cc-content">
|
||||
<div className="lp-cc-badge">推荐</div>
|
||||
<h3 className="lp-cc-title">{b.title}</h3>
|
||||
{b.linkAppName && <p className="lp-cc-sub">立即体验 {b.linkAppName}</p>}
|
||||
<button className="lp-cc-cta" onClick={goLogin}>查看详情 <Icon d={<polyline points="9 18 15 12 9 6" />} size={14} /></button>
|
||||
</div>
|
||||
<div className="lp-cc-visual"><span className="c1" /><span className="c2" /><span className="c3" /></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{banners.length > 1 && (
|
||||
<>
|
||||
<button className="lp-arrow lp-arrow-l" onClick={() => setCurrentSlide(p => (p - 1 + banners.length) % banners.length)} aria-label="上一张"><Icon d={<polyline points="15 18 9 12 15 6" />} size={20} /></button>
|
||||
<button className="lp-arrow lp-arrow-r" onClick={nextSlide} aria-label="下一张"><Icon d={<polyline points="9 18 15 12 9 6" />} size={20} /></button>
|
||||
<div className="lp-dots">{banners.map((_, i) => (<button key={i} className={`lp-dot ${i === currentSlide ? 'on' : ''}`} onClick={() => setCurrentSlide(i)} aria-label={`第${i + 1}张`} />))}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 平台定位(痛点) */}
|
||||
<section className="lp-section" id="positioning">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-section-head">
|
||||
<h2 className="lp-h2">我们解决什么</h2>
|
||||
<p className="lp-sub">七虎AI 帮你绕过个人与职场使用 AI 时最高频的痛点</p>
|
||||
</div>
|
||||
<div className="lp-grid lp-grid-4">
|
||||
{painPoints.map((p, i) => (
|
||||
<article key={i} className="lp-pain">
|
||||
<div className="lp-pain-icon"><Icon d={p.d} size={24} /></div>
|
||||
<h3 className="lp-pain-title">{p.title}</h3>
|
||||
<p className="lp-pain-desc">{p.desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Type Filter */}
|
||||
<div className="market-filter-tabs">
|
||||
<button
|
||||
className={`filter-tab ${appTypeFilter === '' ? 'active' : ''}`}
|
||||
onClick={() => setAppTypeFilter('')}
|
||||
>全部</button>
|
||||
<button
|
||||
className={`filter-tab ${appTypeFilter === 'skill' ? 'active' : ''}`}
|
||||
onClick={() => setAppTypeFilter('skill')}
|
||||
>Skills</button>
|
||||
<button
|
||||
className={`filter-tab ${appTypeFilter === 'agent' ? 'active' : ''}`}
|
||||
onClick={() => setAppTypeFilter('agent')}
|
||||
>Agent</button>
|
||||
<button
|
||||
className={`filter-tab ${appTypeFilter === 'mcp' ? 'active' : ''}`}
|
||||
onClick={() => setAppTypeFilter('mcp')}
|
||||
>MCP</button>
|
||||
{/* 平台价值 */}
|
||||
<section className="lp-section lp-section-alt" id="value">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-section-head">
|
||||
<h2 className="lp-h2">在七虎AI 你能获得</h2>
|
||||
<p className="lp-sub">降低订阅成本、提供无限 Token、垂直模板与可交易市场</p>
|
||||
</div>
|
||||
<div className="lp-grid lp-grid-5">
|
||||
{valueProps.map((v, i) => (
|
||||
<article key={i} className="lp-value">
|
||||
<div className="lp-value-icon"><Icon d={v.d} size={24} /></div>
|
||||
<h3 className="lp-value-title">{v.kw ? <>{v.title.replace(v.kw, '')}<span className="kw"> {v.kw}</span></> : v.title}</h3>
|
||||
<p className="lp-value-desc">{v.desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* App Grid */}
|
||||
<div className="market-app-grid">
|
||||
{filteredApps.map(app => (
|
||||
<div key={app.id} className="market-app-card" onClick={() => handleAppClick(app.name)}>
|
||||
<div className="app-card-cover" style={{ background: getGradient(app) }}>
|
||||
<span className="app-card-type-badge" style={{ background: typeColors[app.type] }}>
|
||||
{typeLabels[app.type]}
|
||||
{/* 热门技能榜 Top5 */}
|
||||
<section className="lp-section" id="top">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-section-head">
|
||||
<h2 className="lp-h2">热门技能榜 · Top 5</h2>
|
||||
<p className="lp-sub">按下载 / 安装量排行,看看大家都在用哪些能力</p>
|
||||
</div>
|
||||
<ol className="lp-rank">
|
||||
{topSkills.map((app, i) => (
|
||||
<li key={app.id} className="lp-rank-item" onClick={goLogin} role="button" tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && goLogin()}>
|
||||
<span className={`lp-rank-no ${i === 0 ? 'top' : ''}`}>{i + 1}</span>
|
||||
<span className="lp-rank-cover" style={{ background: gradFor(app.id) }}>
|
||||
<span className={`lp-rank-badge ${typeBadge[app.type]}`}>{typeLabels[app.type]}</span>
|
||||
</span>
|
||||
{app.price > 0 && (
|
||||
<span className="app-card-price">{app.price} 元</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="app-card-body">
|
||||
<h4 className="app-card-name">{app.name}</h4>
|
||||
<p className="app-card-desc">{app.description}</p>
|
||||
<div className="app-card-meta">
|
||||
<span className="app-card-rating">
|
||||
<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 > 0 ? app.ratingAvg : '-'}
|
||||
</span>
|
||||
<span className="app-card-installs">{app.installCount.toLocaleString()} 次安装</span>
|
||||
<span className="lp-rank-main">
|
||||
<span className="lp-rank-name">{app.name}</span>
|
||||
<span className="lp-rank-desc">{app.description}</span>
|
||||
</span>
|
||||
<span className="lp-rank-meta">
|
||||
<span className="lp-rank-installs"><Icon d={<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" />} size={13} /> {app.installCount.toLocaleString()}</span>
|
||||
<span className="lp-rank-rating">{(app.ratingAvg > 0 ? app.ratingAvg : '—')}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 应用市场预览 */}
|
||||
<section className="lp-section lp-section-alt" id="market">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-section-head">
|
||||
<h2 className="lp-h2">应用市场</h2>
|
||||
<p className="lp-sub">丰富的 Skills、Agent、MCP 应用,一键接入你的工作流</p>
|
||||
</div>
|
||||
<div className="lp-app-grid">
|
||||
{publishedApps.map(app => (
|
||||
<article key={app.id} className="lp-app-card" onClick={goLogin} role="button" tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && goLogin()}>
|
||||
<div className="lp-app-cover" style={{ background: gradFor(app.id) }}>
|
||||
<span className={`lp-app-badge ${typeBadge[app.type]}`}>{typeLabels[app.type]}</span>
|
||||
{app.price > 0 && <span className="lp-app-price">{app.price} 元</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="market-more">
|
||||
<button className="landing-btn landing-btn-outline" onClick={() => navigate('/login')}>
|
||||
登录查看更多应用
|
||||
<SVGIcon size={14}><polyline points="9 18 15 12 9 6"/></SVGIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="landing-features" id="features">
|
||||
<div className="landing-section-inner">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">平台核心能力</h2>
|
||||
<p className="section-desc">从对话到自动化,Qihu-AIFlow 为你提供完整的 AI 工作流解决方案</p>
|
||||
</div>
|
||||
<div className="features-grid">
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">
|
||||
<SVGIcon size={28}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon>
|
||||
</div>
|
||||
<h3 className="feature-title">多模型AI对话</h3>
|
||||
<p className="feature-desc">支持GPT-4o、DeepSeek-R1、Qwen-Max等多种大模型自由切换,按需使用最优模型</p>
|
||||
<div className="lp-app-body">
|
||||
<h4 className="lp-app-name">{app.name}</h4>
|
||||
<p className="lp-app-desc">{app.description}</p>
|
||||
<div className="lp-app-meta">
|
||||
<span><Icon d={<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" />} size={12} /> {app.ratingAvg > 0 ? app.ratingAvg : '-'}</span>
|
||||
<span>{app.installCount.toLocaleString()} 次安装</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">
|
||||
<SVGIcon size={28}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon>
|
||||
</div>
|
||||
<h3 className="feature-title">私有知识库</h3>
|
||||
<p className="feature-desc">上传文档构建专属知识库,AI回答基于你的内容,更精准可靠</p>
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">
|
||||
<SVGIcon size={28}><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon>
|
||||
</div>
|
||||
<h3 className="feature-title">应用市场</h3>
|
||||
<p className="feature-desc">丰富的Skills、Agent、MCP应用,一站式接入工作流,即装即用</p>
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">
|
||||
<SVGIcon size={28}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>
|
||||
</div>
|
||||
<h3 className="feature-title">Token驾驶舱</h3>
|
||||
<p className="feature-desc">实时监控Token消耗,费用透明可控,每一笔都有据可查</p>
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">
|
||||
<SVGIcon size={28}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon>
|
||||
</div>
|
||||
<h3 className="feature-title">IM频道绑定</h3>
|
||||
<p className="feature-desc">支持飞书、钉钉、微信等多平台绑定,AI助手融入日常工作流</p>
|
||||
</div>
|
||||
<div className="feature-card">
|
||||
<div className="feature-icon">
|
||||
<SVGIcon size={28}><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></SVGIcon>
|
||||
</div>
|
||||
<h3 className="feature-title">自动化任务编排</h3>
|
||||
<p className="feature-desc">创建关键词触发、定时执行等自动化任务,让AI按你的节奏工作</p>
|
||||
<div className="lp-more">
|
||||
<button className="lp-btn lp-btn-outline" onClick={goLogin}>登录查看更多应用 <Icon d={<polyline points="9 18 15 12 9 6" />} size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="landing-cta">
|
||||
<div className="landing-section-inner">
|
||||
<h2 className="cta-title">准备好开始了吗?</h2>
|
||||
<p className="cta-desc">免费注册即可体验全部功能,无需任何技术背景</p>
|
||||
<button className="landing-btn landing-btn-primary landing-btn-lg" onClick={() => navigate('/login')}>
|
||||
免费注册,立即体验
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{/* 平台能力 */}
|
||||
<section className="lp-section" id="features">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-section-head">
|
||||
<h2 className="lp-h2">平台核心能力</h2>
|
||||
<p className="lp-sub">从对话到自动化,七虎AI 提供完整的 AI 工作流解决方案</p>
|
||||
</div>
|
||||
<div className="lp-grid lp-grid-3">
|
||||
{features.map((f, i) => (
|
||||
<article key={i} className="lp-feature">
|
||||
<div className="lp-feature-icon"><Icon d={f.d} size={26} /></div>
|
||||
<h3 className="lp-feature-title">{f.title}</h3>
|
||||
<p className="lp-feature-desc">{f.desc}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="landing-footer">
|
||||
<div className="landing-section-inner">
|
||||
<div className="footer-content">
|
||||
<div className="footer-brand">
|
||||
<div className="landing-brand">
|
||||
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
|
||||
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
|
||||
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||
{/* CTA */}
|
||||
<section className="lp-cta">
|
||||
<div className="lp-inner">
|
||||
<h2 className="lp-cta-title">准备好开始了吗?</h2>
|
||||
<p className="lp-cta-desc">免费注册即可体验全部功能,无需任何技术背景</p>
|
||||
<button className="lp-btn lp-btn-primary lp-btn-lg" onClick={goLogin}>免费注册,立即体验</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Footer(SEO 链接 + 富媒体) */}
|
||||
<footer className="lp-footer">
|
||||
<div className="lp-inner">
|
||||
<div className="lp-footer-grid">
|
||||
<div className="lp-footer-brand">
|
||||
<div className="lp-brand" onClick={goTop} role="button" tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && goTop()} aria-label="回到顶部">
|
||||
<svg width="26" height="26" viewBox="0 0 32 32" fill="none" aria-hidden>
|
||||
<rect width="32" height="32" rx="8" fill="#5B5FED" />
|
||||
<path d="M8 20V12l4 4 4-4v8" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
<span className="brand-name" style={{ color: 'var(--color-text-primary)' }}>Qihu-AIFlow</span>
|
||||
</div>
|
||||
<p style={{ color: 'var(--color-text-tertiary)', fontSize: 'var(--text-sm)', marginTop: 'var(--space-md)' }}>
|
||||
AI智能自动化PaaS平台
|
||||
</p>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<div className="footer-col">
|
||||
<h4>产品</h4>
|
||||
<a>AI聊天</a>
|
||||
<a>知识库</a>
|
||||
<a>应用市场</a>
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<h4>支持</h4>
|
||||
<a>帮助文档</a>
|
||||
<a>API参考</a>
|
||||
<a>联系我们</a>
|
||||
<span className="brand-name">七虎AI</span>
|
||||
</div>
|
||||
<p>个人体验 AI、办公提效与 AI 创收的一站式 PaaS 平台。</p>
|
||||
</div>
|
||||
<div className="lp-footer-col"><h4>产品</h4><a href="#features" onClick={anchorClick('features')}>AI 聊天</a><a href="#market" onClick={anchorClick('market')}>应用市场</a><a href="#value" onClick={anchorClick('value')}>技能交易</a></div>
|
||||
<div className="lp-footer-col"><h4>资源</h4><a href="#top" onClick={anchorClick('top')}>热门技能</a><a href="#positioning" onClick={anchorClick('positioning')}>平台定位</a><a onClick={goLogin} role="button">帮助中心</a></div>
|
||||
<div className="lp-footer-col"><h4>公司</h4><a onClick={goLogin} role="button">关于我们</a><a onClick={goLogin} role="button">联系我们</a><a onClick={goLogin} role="button">加入我们</a></div>
|
||||
</div>
|
||||
<div className="footer-bottom">
|
||||
<span>Qihu-AIFlow 2026. All rights reserved.</span>
|
||||
</div>
|
||||
<div className="lp-footer-bottom">© 2026 七虎AI · 让每个人都能用 AI 把事做成</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
+1637
-1606
File diff suppressed because it is too large.
Load diff
+1609
-1578
File diff suppressed because it is too large.
Load diff
@@ -209,7 +209,7 @@ const PricingPage: React.FC = () => {
|
||||
setShowModal(null);
|
||||
// 跳转到聊天页面,带定制服务ID
|
||||
setTimeout(() => {
|
||||
window.location.hash = '#/chat?service_id=' + res.data.id;
|
||||
window.location.href = '/chat?service_id=' + res.data.id;
|
||||
}, 1500);
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -678,8 +678,8 @@ const MembershipContent: React.FC<MembershipContentProps> = ({ user, memberPlan
|
||||
</div>
|
||||
|
||||
<div className="membership-actions">
|
||||
<button className="btn-primary" onClick={() => { window.location.hash = '#/pricing'; }}>续费会员</button>
|
||||
<button className="btn-secondary" onClick={() => { window.location.hash = '#/pricing'; }}>升级会员</button>
|
||||
<button className="btn-primary" onClick={() => { window.location.href = '/pricing'; }}>续费会员</button>
|
||||
<button className="btn-secondary" onClick={() => { window.location.href = '/pricing'; }}>升级会员</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -692,7 +692,7 @@ const MembershipContent: React.FC<MembershipContentProps> = ({ user, memberPlan
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.location.hash = '#/pricing';
|
||||
window.location.href = '/pricing';
|
||||
}}
|
||||
>
|
||||
开通会员
|
||||
|
||||
Reference in new issue
Block a user