feat: support static website

This commit is contained in:
Henry Li
2026-01-24 18:01:27 +08:00
parent c66995bcc0
commit ebda30c7cf
36 changed files with 4889 additions and 92 deletions

View File

@@ -4,6 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"demo:save": "node scripts/save-demo.js",
"build": "next build",
"check": "next lint && tsc --noEmit",
"dev": "next dev --turbo",
@@ -56,6 +57,7 @@
"cmdk": "^1.1.1",
"codemirror": "^6.0.2",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"gsap": "^3.13.0",
"hast": "^1.0.0",

View File

@@ -128,6 +128,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
dotenv:
specifier: ^17.2.3
version: 17.2.3
embla-carousel-react:
specifier: ^8.6.0
version: 8.6.0(react@19.2.3)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>">

View File

@@ -0,0 +1,365 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>江苏城市足球联赛2025赛季 | 苏超联赛第一季</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Oswald:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>">
</head>
<body>
<!-- 加载动画 -->
<div class="loader">
<div class="loader-content">
<div class="football"></div>
<div class="loader-text">加载中...</div>
</div>
</div>
<!-- 导航栏 -->
<nav class="navbar">
<div class="container">
<div class="nav-brand">
<div class="logo">
<div class="logo-ball"></div>
<span class="logo-text">苏超联赛</span>
</div>
<div class="league-name">江苏城市足球联赛2025赛季</div>
</div>
<div class="nav-menu">
<a href="#home" class="nav-link active">首页</a>
<a href="#teams" class="nav-link">球队</a>
<a href="#fixtures" class="nav-link">赛程</a>
<a href="#standings" class="nav-link">积分榜</a>
<a href="#stats" class="nav-link">数据</a>
<a href="#news" class="nav-link">新闻</a>
</div>
<div class="nav-actions">
<button class="btn-theme-toggle">
<i class="fas fa-moon"></i>
</button>
<button class="btn-menu-toggle">
<i class="fas fa-bars"></i>
</button>
</div>
</div>
</nav>
<!-- 主内容区 -->
<main>
<!-- 英雄区域 -->
<section id="home" class="hero">
<div class="hero-background">
<div class="hero-gradient"></div>
<div class="hero-pattern"></div>
<div class="hero-ball-animation"></div>
</div>
<div class="container">
<div class="hero-content">
<div class="hero-badge">
<span class="badge-season">2025赛季</span>
<span class="badge-league">苏超联赛第一季</span>
</div>
<h1 class="hero-title">
<span class="title-line">江苏城市</span>
<span class="title-line highlight">足球联赛</span>
</h1>
<p class="hero-subtitle">
江苏省首个城市间职业足球联赛汇集12支精英球队点燃2025赛季战火
</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-number">12</div>
<div class="stat-label">参赛球队</div>
</div>
<div class="stat-item">
<div class="stat-number">132</div>
<div class="stat-label">场比赛</div>
</div>
<div class="stat-item">
<div class="stat-number">26</div>
<div class="stat-label">比赛周</div>
</div>
<div class="stat-item">
<div class="stat-number">1</div>
<div class="stat-label">冠军荣耀</div>
</div>
</div>
<div class="hero-actions">
<a href="#fixtures" class="btn btn-primary">
<i class="fas fa-calendar-alt"></i>
查看赛程
</a>
<a href="#standings" class="btn btn-secondary">
<i class="fas fa-trophy"></i>
积分榜
</a>
</div>
</div>
<div class="hero-visual">
<div class="stadium-visual">
<div class="stadium-field"></div>
<div class="stadium-stands"></div>
<div class="stadium-players">
<div class="player player-1"></div>
<div class="player player-2"></div>
<div class="player player-3"></div>
</div>
<div class="stadium-ball"></div>
</div>
</div>
</div>
<div class="hero-scroll">
<div class="scroll-indicator">
<div class="scroll-line"></div>
</div>
</div>
</section>
<!-- 下一场比赛 -->
<section class="next-match">
<div class="container">
<div class="section-header">
<h2 class="section-title">下一场比赛</h2>
<div class="section-subtitle">即将开始的精彩对决</div>
</div>
<div class="match-card">
<div class="match-date">
<div class="match-day">周六</div>
<div class="match-date-number">25</div>
<div class="match-month">一月</div>
<div class="match-time">19:30</div>
</div>
<div class="match-teams">
<div class="team team-home">
<div class="team-logo logo-nanjing"></div>
<div class="team-name">南京城联</div>
<div class="team-record">8胜 3平 2负</div>
</div>
<div class="match-vs">
<div class="vs-text">VS</div>
<div class="match-info">
<div class="match-venue">南京奥体中心</div>
<div class="match-round">第12轮</div>
</div>
</div>
<div class="team team-away">
<div class="team-logo logo-suzhou"></div>
<div class="team-name">苏州雄狮</div>
<div class="team-record">7胜 4平 2负</div>
</div>
</div>
<div class="match-actions">
<button class="btn btn-outline">
<i class="fas fa-bell"></i>
设置提醒
</button>
<button class="btn btn-primary">
<i class="fas fa-ticket-alt"></i>
购票
</button>
</div>
</div>
</div>
</section>
<!-- 球队展示 -->
<section id="teams" class="teams-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">参赛球队</h2>
<div class="section-subtitle">12支城市代表队的荣耀之战</div>
</div>
<div class="teams-grid">
<!-- 球队卡片将通过JS动态生成 -->
</div>
</div>
</section>
<!-- 积分榜 -->
<section id="standings" class="standings-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">积分榜</h2>
<div class="section-subtitle">2025赛季实时排名</div>
</div>
<div class="standings-container">
<div class="standings-table">
<table>
<thead>
<tr>
<th>排名</th>
<th>球队</th>
<th>场次</th>
<th></th>
<th></th>
<th></th>
<th>进球</th>
<th>失球</th>
<th>净胜球</th>
<th>积分</th>
</tr>
</thead>
<tbody>
<!-- 积分榜数据将通过JS动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- 赛程表 -->
<section id="fixtures" class="fixtures-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">赛程表</h2>
<div class="section-subtitle">2025赛季完整赛程</div>
</div>
<div class="fixtures-tabs">
<div class="tabs">
<button class="tab active" data-round="all">全部赛程</button>
<button class="tab" data-round="next">即将比赛</button>
<button class="tab" data-round="recent">最近赛果</button>
</div>
<div class="fixtures-list">
<!-- 赛程数据将通过JS动态生成 -->
</div>
</div>
</div>
</section>
<!-- 数据统计 -->
<section id="stats" class="stats-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">数据统计</h2>
<div class="section-subtitle">球员与球队数据排行榜</div>
</div>
<div class="stats-tabs">
<div class="stats-tab-nav">
<button class="stats-tab active" data-tab="scorers">射手榜</button>
<button class="stats-tab" data-tab="assists">助攻榜</button>
<button class="stats-tab" data-tab="teams">球队数据</button>
</div>
<div class="stats-content">
<div class="stats-tab-content active" id="scorers">
<!-- 射手榜数据 -->
</div>
<div class="stats-tab-content" id="assists">
<!-- 助攻榜数据 -->
</div>
<div class="stats-tab-content" id="teams">
<!-- 球队数据 -->
</div>
</div>
</div>
</div>
</section>
<!-- 新闻动态 -->
<section id="news" class="news-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">新闻动态</h2>
<div class="section-subtitle">联赛最新资讯</div>
</div>
<div class="news-grid">
<!-- 新闻卡片将通过JS动态生成 -->
</div>
</div>
</section>
<!-- 底部 -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-brand">
<div class="logo">
<div class="logo-ball"></div>
<span class="logo-text">苏超联赛</span>
</div>
<div class="footer-description">
江苏城市足球联赛2025赛季官方网站
</div>
<div class="footer-social">
<a href="#" class="social-link"><i class="fab fa-weibo"></i></a>
<a href="#" class="social-link"><i class="fab fa-weixin"></i></a>
<a href="#" class="social-link"><i class="fab fa-douyin"></i></a>
<a href="#" class="social-link"><i class="fab fa-bilibili"></i></a>
</div>
</div>
<div class="footer-links">
<div class="footer-column">
<h3 class="footer-title">联赛信息</h3>
<a href="#" class="footer-link">关于联赛</a>
<a href="#" class="footer-link">联赛章程</a>
<a href="#" class="footer-link">组织机构</a>
<a href="#" class="footer-link">合作伙伴</a>
</div>
<div class="footer-column">
<h3 class="footer-title">球迷服务</h3>
<a href="#" class="footer-link">票务信息</a>
<a href="#" class="footer-link">球迷社区</a>
<a href="#" class="footer-link">官方商店</a>
<a href="#" class="footer-link">联系我们</a>
</div>
<div class="footer-column">
<h3 class="footer-title">媒体中心</h3>
<a href="#" class="footer-link">新闻发布</a>
<a href="#" class="footer-link">媒体资料</a>
<a href="#" class="footer-link">采访申请</a>
<a href="#" class="footer-link">摄影图库</a>
</div>
</div>
</div>
<div class="footer-bottom">
<div class="copyright">
&copy; 2025 江苏城市足球联赛. 保留所有权利.
</div>
<div class="footer-legal">
<a href="#" class="legal-link">隐私政策</a>
<a href="#" class="legal-link">使用条款</a>
<a href="#" class="legal-link">Cookie政策</a>
</div>
</div>
</div>
</footer>
</main>
<!-- JavaScript文件 -->
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="js/data.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,802 @@
// 江苏城市足球联赛2025赛季 - 数据文件
const leagueData = {
// 联赛信息
leagueInfo: {
name: "江苏城市足球联赛",
season: "2025赛季",
alias: "苏超联赛第一季",
teamsCount: 12,
totalMatches: 132,
weeks: 26,
startDate: "2025-03-01",
endDate: "2025-10-31"
},
// 参赛球队
teams: [
{
id: 1,
name: "南京城联",
city: "南京",
shortName: "NJL",
colors: ["#dc2626", "#ef4444"],
founded: 2020,
stadium: "南京奥体中心",
capacity: 62000,
manager: "张伟",
captain: "李明"
},
{
id: 2,
name: "苏州雄狮",
city: "苏州",
shortName: "SZS",
colors: ["#059669", "#10b981"],
founded: 2019,
stadium: "苏州奥林匹克体育中心",
capacity: 45000,
manager: "王强",
captain: "陈浩"
},
{
id: 3,
name: "无锡太湖",
city: "无锡",
shortName: "WXT",
colors: ["#3b82f6", "#60a5fa"],
founded: 2021,
stadium: "无锡体育中心",
capacity: 32000,
manager: "赵刚",
captain: "刘洋"
},
{
id: 4,
name: "常州龙城",
city: "常州",
shortName: "CZL",
colors: ["#7c3aed", "#8b5cf6"],
founded: 2022,
stadium: "常州奥林匹克体育中心",
capacity: 38000,
manager: "孙磊",
captain: "周涛"
},
{
id: 5,
name: "镇江金山",
city: "镇江",
shortName: "ZJJ",
colors: ["#f59e0b", "#fbbf24"],
founded: 2020,
stadium: "镇江体育会展中心",
capacity: 28000,
manager: "吴斌",
captain: "郑军"
},
{
id: 6,
name: "扬州运河",
city: "扬州",
shortName: "YZY",
colors: ["#ec4899", "#f472b6"],
founded: 2021,
stadium: "扬州体育公园",
capacity: 35000,
manager: "钱勇",
captain: "王磊"
},
{
id: 7,
name: "南通江海",
city: "南通",
shortName: "NTJ",
colors: ["#0ea5e9", "#38bdf8"],
founded: 2022,
stadium: "南通体育会展中心",
capacity: 32000,
manager: "冯超",
captain: "张勇"
},
{
id: 8,
name: "徐州楚汉",
city: "徐州",
shortName: "XZC",
colors: ["#84cc16", "#a3e635"],
founded: 2019,
stadium: "徐州奥体中心",
capacity: 42000,
manager: "陈明",
captain: "李强"
},
{
id: 9,
name: "淮安运河",
city: "淮安",
shortName: "HAY",
colors: ["#f97316", "#fb923c"],
founded: 2021,
stadium: "淮安体育中心",
capacity: 30000,
manager: "周伟",
captain: "吴刚"
},
{
id: 10,
name: "盐城黄海",
city: "盐城",
shortName: "YCH",
colors: ["#06b6d4", "#22d3ee"],
founded: 2020,
stadium: "盐城体育中心",
capacity: 32000,
manager: "郑涛",
captain: "孙明"
},
{
id: 11,
name: "泰州凤城",
city: "泰州",
shortName: "TZF",
colors: ["#8b5cf6", "#a78bfa"],
founded: 2022,
stadium: "泰州体育公园",
capacity: 28000,
manager: "王刚",
captain: "陈涛"
},
{
id: 12,
name: "宿迁西楚",
city: "宿迁",
shortName: "SQC",
colors: ["#10b981", "#34d399"],
founded: 2021,
stadium: "宿迁体育中心",
capacity: 26000,
manager: "李伟",
captain: "张刚"
}
],
// 积分榜数据
standings: [
{
rank: 1,
teamId: 1,
played: 13,
won: 8,
drawn: 3,
lost: 2,
goalsFor: 24,
goalsAgainst: 12,
goalDifference: 12,
points: 27
},
{
rank: 2,
teamId: 2,
played: 13,
won: 7,
drawn: 4,
lost: 2,
goalsFor: 22,
goalsAgainst: 14,
goalDifference: 8,
points: 25
},
{
rank: 3,
teamId: 8,
played: 13,
won: 7,
drawn: 3,
lost: 3,
goalsFor: 20,
goalsAgainst: 15,
goalDifference: 5,
points: 24
},
{
rank: 4,
teamId: 3,
played: 13,
won: 6,
drawn: 4,
lost: 3,
goalsFor: 18,
goalsAgainst: 14,
goalDifference: 4,
points: 22
},
{
rank: 5,
teamId: 4,
played: 13,
won: 6,
drawn: 3,
lost: 4,
goalsFor: 19,
goalsAgainst: 16,
goalDifference: 3,
points: 21
},
{
rank: 6,
teamId: 6,
played: 13,
won: 5,
drawn: 5,
lost: 3,
goalsFor: 17,
goalsAgainst: 15,
goalDifference: 2,
points: 20
},
{
rank: 7,
teamId: 5,
played: 13,
won: 5,
drawn: 4,
lost: 4,
goalsFor: 16,
goalsAgainst: 15,
goalDifference: 1,
points: 19
},
{
rank: 8,
teamId: 7,
played: 13,
won: 4,
drawn: 5,
lost: 4,
goalsFor: 15,
goalsAgainst: 16,
goalDifference: -1,
points: 17
},
{
rank: 9,
teamId: 10,
played: 13,
won: 4,
drawn: 4,
lost: 5,
goalsFor: 14,
goalsAgainst: 17,
goalDifference: -3,
points: 16
},
{
rank: 10,
teamId: 9,
played: 13,
won: 3,
drawn: 5,
lost: 5,
goalsFor: 13,
goalsAgainst: 18,
goalDifference: -5,
points: 14
},
{
rank: 11,
teamId: 11,
played: 13,
won: 2,
drawn: 4,
lost: 7,
goalsFor: 11,
goalsAgainst: 20,
goalDifference: -9,
points: 10
},
{
rank: 12,
teamId: 12,
played: 13,
won: 1,
drawn: 3,
lost: 9,
goalsFor: 9,
goalsAgainst: 24,
goalDifference: -15,
points: 6
}
],
// 赛程数据
fixtures: [
{
id: 1,
round: 1,
date: "2025-03-01",
time: "15:00",
homeTeamId: 1,
awayTeamId: 2,
venue: "南京奥体中心",
status: "completed",
homeScore: 2,
awayScore: 1
},
{
id: 2,
round: 1,
date: "2025-03-01",
time: "15:00",
homeTeamId: 3,
awayTeamId: 4,
venue: "无锡体育中心",
status: "completed",
homeScore: 1,
awayScore: 1
},
{
id: 3,
round: 1,
date: "2025-03-02",
time: "19:30",
homeTeamId: 5,
awayTeamId: 6,
venue: "镇江体育会展中心",
status: "completed",
homeScore: 0,
awayScore: 2
},
{
id: 4,
round: 1,
date: "2025-03-02",
time: "19:30",
homeTeamId: 7,
awayTeamId: 8,
venue: "南通体育会展中心",
status: "completed",
homeScore: 1,
awayScore: 3
},
{
id: 5,
round: 1,
date: "2025-03-03",
time: "15:00",
homeTeamId: 9,
awayTeamId: 10,
venue: "淮安体育中心",
status: "completed",
homeScore: 2,
awayScore: 2
},
{
id: 6,
round: 1,
date: "2025-03-03",
time: "15:00",
homeTeamId: 11,
awayTeamId: 12,
venue: "泰州体育公园",
status: "completed",
homeScore: 1,
awayScore: 0
},
{
id: 7,
round: 2,
date: "2025-03-08",
time: "15:00",
homeTeamId: 2,
awayTeamId: 3,
venue: "苏州奥林匹克体育中心",
status: "completed",
homeScore: 2,
awayScore: 0
},
{
id: 8,
round: 2,
date: "2025-03-08",
time: "15:00",
homeTeamId: 4,
awayTeamId: 5,
venue: "常州奥林匹克体育中心",
status: "completed",
homeScore: 3,
awayScore: 1
},
{
id: 9,
round: 2,
date: "2025-03-09",
time: "19:30",
homeTeamId: 6,
awayTeamId: 7,
venue: "扬州体育公园",
status: "completed",
homeScore: 1,
awayScore: 1
},
{
id: 10,
round: 2,
date: "2025-03-09",
time: "19:30",
homeTeamId: 8,
awayTeamId: 9,
venue: "徐州奥体中心",
status: "completed",
homeScore: 2,
awayScore: 0
},
{
id: 11,
round: 2,
date: "2025-03-10",
time: "15:00",
homeTeamId: 10,
awayTeamId: 11,
venue: "盐城体育中心",
status: "completed",
homeScore: 1,
awayScore: 0
},
{
id: 12,
round: 2,
date: "2025-03-10",
time: "15:00",
homeTeamId: 12,
awayTeamId: 1,
venue: "宿迁体育中心",
status: "completed",
homeScore: 0,
awayScore: 3
},
{
id: 13,
round: 12,
date: "2025-05-24",
time: "19:30",
homeTeamId: 1,
awayTeamId: 2,
venue: "南京奥体中心",
status: "scheduled"
},
{
id: 14,
round: 12,
date: "2025-05-24",
time: "15:00",
homeTeamId: 3,
awayTeamId: 4,
venue: "无锡体育中心",
status: "scheduled"
},
{
id: 15,
round: 12,
date: "2025-05-25",
time: "19:30",
homeTeamId: 5,
awayTeamId: 6,
venue: "镇江体育会展中心",
status: "scheduled"
},
{
id: 16,
round: 12,
date: "2025-05-25",
time: "15:00",
homeTeamId: 7,
awayTeamId: 8,
venue: "南通体育会展中心",
status: "scheduled"
},
{
id: 17,
round: 12,
date: "2025-05-26",
time: "19:30",
homeTeamId: 9,
awayTeamId: 10,
venue: "淮安体育中心",
status: "scheduled"
},
{
id: 18,
round: 12,
date: "2025-05-26",
time: "15:00",
homeTeamId: 11,
awayTeamId: 12,
venue: "泰州体育公园",
status: "scheduled"
}
],
// 球员数据
players: {
scorers: [
{
rank: 1,
playerId: 101,
name: "张伟",
teamId: 1,
goals: 12,
assists: 4,
matches: 13,
minutes: 1170
},
{
rank: 2,
playerId: 102,
name: "李明",
teamId: 1,
goals: 8,
assists: 6,
matches: 13,
minutes: 1170
},
{
rank: 3,
playerId: 201,
name: "王强",
teamId: 2,
goals: 7,
assists: 5,
matches: 13,
minutes: 1170
},
{
rank: 4,
playerId: 301,
name: "赵刚",
teamId: 3,
goals: 6,
assists: 3,
matches: 13,
minutes: 1170
},
{
rank: 5,
playerId: 801,
name: "陈明",
teamId: 8,
goals: 6,
assists: 2,
matches: 13,
minutes: 1170
},
{
rank: 6,
playerId: 401,
name: "孙磊",
teamId: 4,
goals: 5,
assists: 4,
matches: 13,
minutes: 1170
},
{
rank: 7,
playerId: 601,
name: "钱勇",
teamId: 6,
goals: 5,
assists: 3,
matches: 13,
minutes: 1170
},
{
rank: 8,
playerId: 501,
name: "吴斌",
teamId: 5,
goals: 4,
assists: 5,
matches: 13,
minutes: 1170
},
{
rank: 9,
playerId: 701,
name: "冯超",
teamId: 7,
goals: 4,
assists: 3,
matches: 13,
minutes: 1170
},
{
rank: 10,
playerId: 1001,
name: "郑涛",
teamId: 10,
goals: 3,
assists: 2,
matches: 13,
minutes: 1170
}
],
assists: [
{
rank: 1,
playerId: 102,
name: "李明",
teamId: 1,
assists: 6,
goals: 8,
matches: 13,
minutes: 1170
},
{
rank: 2,
playerId: 501,
name: "吴斌",
teamId: 5,
assists: 5,
goals: 4,
matches: 13,
minutes: 1170
},
{
rank: 3,
playerId: 201,
name: "王强",
teamId: 2,
assists: 5,
goals: 7,
matches: 13,
minutes: 1170
},
{
rank: 4,
playerId: 401,
name: "孙磊",
teamId: 4,
assists: 4,
goals: 5,
matches: 13,
minutes: 1170
},
{
rank: 5,
playerId: 101,
name: "张伟",
teamId: 1,
assists: 4,
goals: 12,
matches: 13,
minutes: 1170
},
{
rank: 6,
playerId: 301,
name: "赵刚",
teamId: 3,
assists: 3,
goals: 6,
matches: 13,
minutes: 1170
},
{
rank: 7,
playerId: 601,
name: "钱勇",
teamId: 6,
assists: 3,
goals: 5,
matches: 13,
minutes: 1170
},
{
rank: 8,
playerId: 701,
name: "冯超",
teamId: 7,
assists: 3,
goals: 4,
matches: 13,
minutes: 1170
},
{
rank: 9,
playerId: 901,
name: "周伟",
teamId: 9,
assists: 3,
goals: 2,
matches: 13,
minutes: 1170
},
{
rank: 10,
playerId: 1101,
name: "王刚",
teamId: 11,
assists: 2,
goals: 1,
matches: 13,
minutes: 1170
}
]
},
// 新闻数据
news: [
{
id: 1,
title: "南京城联主场力克苏州雄狮,继续领跑积分榜",
excerpt: "在昨晚进行的第12轮焦点战中南京城联凭借张伟的梅开二度主场2-1战胜苏州雄狮继续以2分优势领跑积分榜。",
category: "比赛战报",
date: "2025-05-25",
imageColor: "#dc2626"
},
{
id: 2,
title: "联赛最佳球员揭晓张伟当选4月最佳",
excerpt: "江苏城市足球联赛官方宣布南京城联前锋张伟凭借出色的表现当选4月份联赛最佳球员。",
category: "官方公告",
date: "2025-05-20",
imageColor: "#3b82f6"
},
{
id: 3,
title: "徐州楚汉签下前国脚李强,实力大增",
excerpt: "徐州楚汉俱乐部官方宣布,与前国家队中场李强签约两年,这位经验丰富的老将将提升球队中场实力。",
category: "转会新闻",
date: "2025-05-18",
imageColor: "#84cc16"
},
{
id: 4,
title: "联赛半程总结:竞争激烈,多队有望争冠",
excerpt: "随着联赛进入半程积分榜前六名球队分差仅7分本赛季冠军争夺异常激烈多支球队都有机会问鼎。",
category: "联赛动态",
date: "2025-05-15",
imageColor: "#f59e0b"
},
{
id: 5,
title: "球迷互动日:各俱乐部将举办开放训练",
excerpt: "为感谢球迷支持,各俱乐部将在本周末举办球迷开放日,球迷可近距离观看球队训练并与球员互动。",
category: "球迷活动",
date: "2025-05-12",
imageColor: "#ec4899"
},
{
id: 6,
title: "技术统计:联赛进球数创历史新高",
excerpt: "本赛季前13轮共打进176球场均2.77球,创下联赛历史同期最高进球纪录,进攻足球成为主流。",
category: "数据统计",
date: "2025-05-10",
imageColor: "#0ea5e9"
}
]
};
// 工具函数根据ID获取球队信息
function getTeamById(teamId) {
return leagueData.teams.find(team => team.id === teamId);
}
// 工具函数:格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
const options = { weekday: 'short', month: 'short', day: 'numeric' };
return date.toLocaleDateString('zh-CN', options);
}
// 工具函数:格式化时间
function formatTime(timeString) {
return timeString;
}
// 导出数据
if (typeof module !== 'undefined' && module.exports) {
module.exports = leagueData;
}

View File

@@ -0,0 +1,618 @@
// 江苏城市足球联赛2025赛季 - 主JavaScript文件
document.addEventListener('DOMContentLoaded', function() {
// 初始化加载动画
initLoader();
// 初始化主题切换
initThemeToggle();
// 初始化导航菜单
initNavigation();
// 初始化滚动监听
initScrollSpy();
// 渲染球队卡片
renderTeams();
// 渲染积分榜
renderStandings();
// 渲染赛程表
renderFixtures();
// 渲染数据统计
renderStats();
// 渲染新闻动态
renderNews();
// 初始化标签页切换
initTabs();
// 初始化移动端菜单
initMobileMenu();
});
// 加载动画
function initLoader() {
const loader = document.querySelector('.loader');
// 模拟加载延迟
setTimeout(() => {
loader.classList.add('loaded');
// 动画结束后隐藏loader
setTimeout(() => {
loader.style.display = 'none';
}, 300);
}, 1500);
}
// 主题切换
function initThemeToggle() {
const themeToggle = document.querySelector('.btn-theme-toggle');
const themeIcon = themeToggle.querySelector('i');
// 检查本地存储的主题偏好
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
// 添加切换动画
themeToggle.style.transform = 'scale(0.9)';
setTimeout(() => {
themeToggle.style.transform = '';
}, 150);
});
function updateThemeIcon(theme) {
if (theme === 'dark') {
themeIcon.className = 'fas fa-sun';
} else {
themeIcon.className = 'fas fa-moon';
}
}
}
// 导航菜单
function initNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetSection = document.querySelector(targetId);
if (targetSection) {
// 更新活动链接
navLinks.forEach(l => l.classList.remove('active'));
this.classList.add('active');
// 平滑滚动到目标区域
window.scrollTo({
top: targetSection.offsetTop - 80,
behavior: 'smooth'
});
// 如果是移动端,关闭菜单
const navMenu = document.querySelector('.nav-menu');
if (navMenu.classList.contains('active')) {
navMenu.classList.remove('active');
}
}
});
});
}
// 滚动监听
function initScrollSpy() {
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('.nav-link');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (scrollY >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${current}`) {
link.classList.add('active');
}
});
});
}
// 渲染球队卡片
function renderTeams() {
const teamsGrid = document.querySelector('.teams-grid');
if (!teamsGrid) return;
teamsGrid.innerHTML = '';
leagueData.teams.forEach(team => {
const teamCard = document.createElement('div');
teamCard.className = 'team-card';
// 获取球队统计数据
const standing = leagueData.standings.find(s => s.teamId === team.id);
teamCard.innerHTML = `
<div class="team-card-logo" style="background: linear-gradient(135deg, ${team.colors[0]} 0%, ${team.colors[1]} 100%);">
${team.shortName}
</div>
<h3 class="team-card-name">${team.name}</h3>
<div class="team-card-city">${team.city}</div>
<div class="team-card-stats">
<div class="team-stat">
<div class="team-stat-value">${standing ? standing.rank : '-'}</div>
<div class="team-stat-label">排名</div>
</div>
<div class="team-stat">
<div class="team-stat-value">${standing ? standing.points : '0'}</div>
<div class="team-stat-label">积分</div>
</div>
<div class="team-stat">
<div class="team-stat-value">${standing ? standing.goalDifference : '0'}</div>
<div class="team-stat-label">净胜球</div>
</div>
</div>
`;
teamCard.addEventListener('click', () => {
// 这里可以添加点击跳转到球队详情页的功能
alert(`查看 ${team.name} 的详细信息`);
});
teamsGrid.appendChild(teamCard);
});
}
// 渲染积分榜
function renderStandings() {
const standingsTable = document.querySelector('.standings-table tbody');
if (!standingsTable) return;
standingsTable.innerHTML = '';
leagueData.standings.forEach(standing => {
const team = getTeamById(standing.teamId);
const row = document.createElement('tr');
// 根据排名添加特殊样式
if (standing.rank <= 4) {
row.classList.add('champions-league');
} else if (standing.rank <= 6) {
row.classList.add('europa-league');
} else if (standing.rank >= 11) {
row.classList.add('relegation');
}
row.innerHTML = `
<td>${standing.rank}</td>
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div class="team-logo-small" style="width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, ${team.colors[0]} 0%, ${team.colors[1]} 100%);"></div>
${team.name}
</div>
</td>
<td>${standing.played}</td>
<td>${standing.won}</td>
<td>${standing.drawn}</td>
<td>${standing.lost}</td>
<td>${standing.goalsFor}</td>
<td>${standing.goalsAgainst}</td>
<td>${standing.goalDifference > 0 ? '+' : ''}${standing.goalDifference}</td>
<td><strong>${standing.points}</strong></td>
`;
standingsTable.appendChild(row);
});
}
// 渲染赛程表
function renderFixtures() {
const fixturesList = document.querySelector('.fixtures-list');
if (!fixturesList) return;
fixturesList.innerHTML = '';
// 按轮次分组
const fixturesByRound = {};
leagueData.fixtures.forEach(fixture => {
if (!fixturesByRound[fixture.round]) {
fixturesByRound[fixture.round] = [];
}
fixturesByRound[fixture.round].push(fixture);
});
// 渲染所有赛程
Object.keys(fixturesByRound).sort((a, b) => a - b).forEach(round => {
const roundHeader = document.createElement('div');
roundHeader.className = 'fixture-round-header';
roundHeader.innerHTML = `<h3>第${round}轮</h3>`;
fixturesList.appendChild(roundHeader);
fixturesByRound[round].forEach(fixture => {
const homeTeam = getTeamById(fixture.homeTeamId);
const awayTeam = getTeamById(fixture.awayTeamId);
const fixtureItem = document.createElement('div');
fixtureItem.className = 'fixture-item';
const date = new Date(fixture.date);
const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const dayName = dayNames[date.getDay()];
let scoreHtml = '';
let statusText = '';
if (fixture.status === 'completed') {
scoreHtml = `
<div class="fixture-score-value">${fixture.homeScore} - ${fixture.awayScore}</div>
<div class="fixture-score-status">已结束</div>
`;
} else if (fixture.status === 'scheduled') {
scoreHtml = `
<div class="fixture-score-value">VS</div>
<div class="fixture-score-status">${fixture.time}</div>
`;
} else {
scoreHtml = `
<div class="fixture-score-value">-</div>
<div class="fixture-score-status">待定</div>
`;
}
fixtureItem.innerHTML = `
<div class="fixture-date">
<div class="fixture-day">${dayName}</div>
<div class="fixture-time">${formatDate(fixture.date)}</div>
</div>
<div class="fixture-teams">
<div class="fixture-team home">
<div class="fixture-team-name">${homeTeam.name}</div>
<div class="fixture-team-logo" style="background: linear-gradient(135deg, ${homeTeam.colors[0]} 0%, ${homeTeam.colors[1]} 100%);"></div>
</div>
<div class="fixture-vs">VS</div>
<div class="fixture-team away">
<div class="fixture-team-logo" style="background: linear-gradient(135deg, ${awayTeam.colors[0]} 0%, ${awayTeam.colors[1]} 100%);"></div>
<div class="fixture-team-name">${awayTeam.name}</div>
</div>
</div>
<div class="fixture-score">
${scoreHtml}
</div>
`;
fixturesList.appendChild(fixtureItem);
});
});
}
// 渲染数据统计
function renderStats() {
renderScorers();
renderAssists();
renderTeamStats();
}
function renderScorers() {
const scorersContainer = document.querySelector('#scorers');
if (!scorersContainer) return;
scorersContainer.innerHTML = `
<table class="stats-table">
<thead>
<tr>
<th class="stats-rank">排名</th>
<th class="stats-player">球员</th>
<th class="stats-team">球队</th>
<th class="stats-value">进球</th>
<th class="stats-value">助攻</th>
<th class="stats-value">出场</th>
</tr>
</thead>
<tbody>
${leagueData.players.scorers.map(player => {
const team = getTeamById(player.teamId);
return `
<tr>
<td class="stats-rank">${player.rank}</td>
<td class="stats-player">${player.name}</td>
<td class="stats-team">${team.name}</td>
<td class="stats-value">${player.goals}</td>
<td class="stats-value">${player.assists}</td>
<td class="stats-value">${player.matches}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
function renderAssists() {
const assistsContainer = document.querySelector('#assists');
if (!assistsContainer) return;
assistsContainer.innerHTML = `
<table class="stats-table">
<thead>
<tr>
<th class="stats-rank">排名</th>
<th class="stats-player">球员</th>
<th class="stats-team">球队</th>
<th class="stats-value">助攻</th>
<th class="stats-value">进球</th>
<th class="stats-value">出场</th>
</tr>
</thead>
<tbody>
${leagueData.players.assists.map(player => {
const team = getTeamById(player.teamId);
return `
<tr>
<td class="stats-rank">${player.rank}</td>
<td class="stats-player">${player.name}</td>
<td class="stats-team">${team.name}</td>
<td class="stats-value">${player.assists}</td>
<td class="stats-value">${player.goals}</td>
<td class="stats-value">${player.matches}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
function renderTeamStats() {
const teamStatsContainer = document.querySelector('#teams');
if (!teamStatsContainer) return;
// 计算球队统计数据
const teamStats = leagueData.standings.map(standing => {
const team = getTeamById(standing.teamId);
const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2);
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(2);
return {
rank: standing.rank,
team: team.name,
goalsFor: standing.goalsFor,
goalsAgainst: standing.goalsAgainst,
goalDifference: standing.goalDifference,
goalsPerGame,
concededPerGame,
cleanSheets: Math.floor(Math.random() * 5) // 模拟数据
};
}).sort((a, b) => a.rank - b.rank);
teamStatsContainer.innerHTML = `
<table class="stats-table">
<thead>
<tr>
<th class="stats-rank">排名</th>
<th class="stats-player">球队</th>
<th class="stats-value">进球</th>
<th class="stats-value">失球</th>
<th class="stats-value">净胜球</th>
<th class="stats-value">场均进球</th>
<th class="stats-value">场均失球</th>
<th class="stats-value">零封</th>
</tr>
</thead>
<tbody>
${teamStats.map(stat => `
<tr>
<td class="stats-rank">${stat.rank}</td>
<td class="stats-player">${stat.team}</td>
<td class="stats-value">${stat.goalsFor}</td>
<td class="stats-value">${stat.goalsAgainst}</td>
<td class="stats-value">${stat.goalDifference > 0 ? '+' : ''}${stat.goalDifference}</td>
<td class="stats-value">${stat.goalsPerGame}</td>
<td class="stats-value">${stat.concededPerGame}</td>
<td class="stats-value">${stat.cleanSheets}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 渲染新闻动态
function renderNews() {
const newsGrid = document.querySelector('.news-grid');
if (!newsGrid) return;
newsGrid.innerHTML = '';
leagueData.news.forEach(newsItem => {
const newsCard = document.createElement('div');
newsCard.className = 'news-card';
const date = new Date(newsItem.date);
const formattedDate = date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
newsCard.innerHTML = `
<div class="news-card-image" style="background: linear-gradient(135deg, ${newsItem.imageColor} 0%, ${darkenColor(newsItem.imageColor, 20)} 100%);"></div>
<div class="news-card-content">
<span class="news-card-category">${newsItem.category}</span>
<h3 class="news-card-title">${newsItem.title}</h3>
<p class="news-card-excerpt">${newsItem.excerpt}</p>
<div class="news-card-meta">
<span class="news-card-date">
<i class="far fa-calendar"></i>
${formattedDate}
</span>
<span class="news-card-read-more">阅读更多 →</span>
</div>
</div>
`;
newsCard.addEventListener('click', () => {
alert(`查看新闻: ${newsItem.title}`);
});
newsGrid.appendChild(newsCard);
});
}
// 初始化标签页切换
function initTabs() {
// 赛程标签页
const fixtureTabs = document.querySelectorAll('.fixtures-tabs .tab');
const fixtureItems = document.querySelectorAll('.fixture-item');
fixtureTabs.forEach(tab => {
tab.addEventListener('click', () => {
// 更新活动标签
fixtureTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const roundFilter = tab.getAttribute('data-round');
// 这里可以根据筛选条件显示不同的赛程
// 由于时间关系,这里只是简单的演示
console.log(`筛选赛程: ${roundFilter}`);
});
});
// 数据统计标签页
const statsTabs = document.querySelectorAll('.stats-tab');
const statsContents = document.querySelectorAll('.stats-tab-content');
statsTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.getAttribute('data-tab');
// 更新活动标签
statsTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// 显示对应内容
statsContents.forEach(content => {
content.classList.remove('active');
if (content.id === tabId) {
content.classList.add('active');
}
});
});
});
}
// 初始化移动端菜单
function initMobileMenu() {
const menuToggle = document.querySelector('.btn-menu-toggle');
const navMenu = document.querySelector('.nav-menu');
if (menuToggle && navMenu) {
menuToggle.addEventListener('click', () => {
navMenu.classList.toggle('active');
// 更新菜单图标
const icon = menuToggle.querySelector('i');
if (navMenu.classList.contains('active')) {
icon.className = 'fas fa-times';
} else {
icon.className = 'fas fa-bars';
}
});
// 点击菜单外区域关闭菜单
document.addEventListener('click', (e) => {
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
navMenu.classList.remove('active');
menuToggle.querySelector('i').className = 'fas fa-bars';
}
});
}
}
// 工具函数:加深颜色
function darkenColor(color, percent) {
const num = parseInt(color.replace("#", ""), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt;
const G = (num >> 8 & 0x00FF) - amt;
const B = (num & 0x0000FF) - amt;
return "#" + (
0x1000000 +
(R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
(B < 255 ? B < 1 ? 0 : B : 255)
).toString(16).slice(1);
}
// 工具函数:格式化日期(简写)
function formatDate(dateString) {
const date = new Date(dateString);
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}${day}`;
}
// 工具函数根据ID获取球队信息
function getTeamById(teamId) {
return leagueData.teams.find(team => team.id === teamId);
}
// 添加一些交互效果
document.addEventListener('DOMContentLoaded', () => {
// 为所有按钮添加点击效果
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('mousedown', () => {
button.style.transform = 'scale(0.95)';
});
button.addEventListener('mouseup', () => {
button.style.transform = '';
});
button.addEventListener('mouseleave', () => {
button.style.transform = '';
});
});
// 为卡片添加悬停效果
const cards = document.querySelectorAll('.team-card, .news-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
});
});
});

View File

@@ -0,0 +1,53 @@
import { config } from "dotenv";
import fs from "fs";
import path from "path";
import { env } from "process";
export async function main() {
const threadId = process.argv[2];
const url = new URL(
`http://localhost:2026/api/langgraph/threads/${threadId}/history`,
);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
limit: 10,
}),
});
const data = (await response.json())[0];
if (!data) {
console.error("No data found");
return;
}
const title = data.values.title;
const rootPath = path.resolve(process.cwd(), "public/demo/threads", threadId);
if (fs.existsSync(rootPath)) {
fs.rmSync(rootPath, { recursive: true });
}
fs.mkdirSync(rootPath, { recursive: true });
fs.writeFileSync(
path.resolve(rootPath, "thread.json"),
JSON.stringify(data, null, 2),
);
const backendRootPath = path.resolve(
process.cwd(),
"../backend/.deer-flow/threads",
threadId,
);
const outputsPath = path.resolve(backendRootPath, "user-data/outputs");
if (fs.existsSync(outputsPath)) {
fs.cpSync(outputsPath, path.resolve(rootPath, "user-data/outputs"), {
recursive: true,
});
}
console.info(`Saved demo "${title}" to ${rootPath}`);
}
config();
main();

View File

@@ -0,0 +1,26 @@
export function GET() {
return Response.json({
mcp_servers: {
"mcp-github-trending": {
enabled: true,
type: "stdio",
command: "uvx",
args: ["mcp-github-trending"],
env: {},
url: null,
headers: {},
description:
"A MCP server that provides access to GitHub trending repositories and developers data",
},
"context-7": {
enabled: true,
description:
"Get the latest documentation and code into Cursor, Claude, or other LLMs",
},
"feishu-importer": {
enabled: true,
description: "Import Feishu documents",
},
},
});
}

View File

@@ -0,0 +1,30 @@
export function GET() {
return Response.json({
models: [
{
id: "doubao-seed-1.8",
name: "doubao-seed-1.8",
display_name: "Doubao Seed 1.8",
supports_thinking: true,
},
{
id: "deepseek-v3.2",
name: "deepseek-v3.2",
display_name: "DeepSeek v3.2",
supports_thinking: true,
},
{
id: "gpt-5",
name: "gpt-5",
display_name: "GPT-5",
supports_thinking: true,
},
{
id: "gemini-3-pro",
name: "gemini-3-pro",
display_name: "Gemini 3 Pro",
supports_thinking: true,
},
],
});
}

View File

@@ -0,0 +1,70 @@
export function GET() {
return Response.json({
skills: [
{
name: "frontend-design",
description:
"Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.",
license: "Complete terms in LICENSE.txt",
category: "public",
enabled: true,
},
{
name: "pdf-processing",
description:
"Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.",
license: "Proprietary. LICENSE.txt has complete terms",
category: "public",
enabled: true,
},
{
name: "vercel-deploy",
description:
'Deploy applications and websites to Vercel. Use this skill when the user requests deployment actions such as "Deploy my app", "Deploy this to production", "Create a preview deployment", "Deploy and give me the link", or "Push this live". No authentication required - returns preview URL and claimable deployment link.',
license: null,
category: "public",
enabled: true,
},
{
name: "web-design-guidelines",
description:
'Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".',
license: null,
category: "public",
enabled: true,
},
{
name: "cartoon-generator",
description:
'Generate cartoon images based on a description. Use when asked to "generate a cartoon image", "create a cartoon", "draw a cartoon", or "generate a cartoon image based on a description".',
license: null,
category: "custom",
enabled: true,
},
{
name: "podcast-generator",
description:
'Generate a podcast episode based on a topic. Use when asked to "generate a podcast episode", "create a podcast episode", "generate a podcast episode based on a topic", or "generate a podcast episode based on a description".',
license: null,
category: "custom",
enabled: true,
},
{
name: "advanced-data-analysis",
description:
'Perform advanced data analysis and visualization. Use when asked to "analyze data", "visualize data", "analyze data based on a description", or "visualize data based on a description".',
license: null,
category: "custom",
enabled: true,
},
{
name: "3d-model-generator",
description:
'Generate 3D models based on a description. Use when asked to "generate a 3D model", "create a 3D model", "generate a 3D model based on a description", or "generate a 3D model based on a description".',
license: null,
category: "custom",
enabled: true,
},
],
});
}

View File

@@ -0,0 +1,41 @@
import fs from "fs";
import path from "path";
import type { NextRequest } from "next/server";
export async function GET(
request: NextRequest,
{
params,
}: {
params: Promise<{
thread_id: string;
artifact_path?: string[] | undefined;
}>;
},
) {
const threadId = (await params).thread_id;
let artifactPath = (await params).artifact_path?.join("/") ?? "";
if (artifactPath.startsWith("mnt/")) {
artifactPath = path.resolve(
process.cwd(),
artifactPath.replace("mnt/", `public/demo/threads/${threadId}/`),
);
if (fs.existsSync(artifactPath)) {
if (request.nextUrl.searchParams.get("download") === "true") {
// Attach the file to the response
const headers = new Headers();
headers.set(
"Content-Disposition",
`attachment; filename="${artifactPath}"`,
);
return new Response(fs.readFileSync(artifactPath), {
status: 200,
headers,
});
}
return new Response(fs.readFileSync(artifactPath), { status: 200 });
}
}
return new Response("File not found", { status: 404 });
}

View File

@@ -0,0 +1,20 @@
import fs from "fs";
import path from "path";
import type { NextRequest } from "next/server";
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ thread_id: string }> },
) {
const threadId = (await params).thread_id;
const jsonString = fs.readFileSync(
path.resolve(process.cwd(), `public/demo/threads/${threadId}/thread.json`),
"utf8",
);
const json = JSON.parse(jsonString);
if (Array.isArray(json.history)) {
return Response.json(json);
}
return Response.json([json]);
}

View File

@@ -0,0 +1,27 @@
import fs from "fs";
import path from "path";
export function POST() {
const threadsDir = fs.readdirSync(
path.resolve(process.cwd(), "public/demo/threads"),
{
withFileTypes: true,
},
);
const threadData = threadsDir
.map((threadId) => {
if (threadId.isDirectory() && !threadId.name.startsWith(".")) {
const threadData = fs.readFileSync(
path.resolve(`public/demo/threads/${threadId.name}/thread.json`),
"utf8",
);
return {
thread_id: threadId.name,
values: JSON.parse(threadData).values,
};
}
return false;
})
.filter(Boolean);
return Response.json(threadData);
}

View File

@@ -30,6 +30,7 @@ import { type AgentThread } from "@/core/threads";
import { useSubmitThread, useThreadStream } from "@/core/threads/hooks";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
import { uuid } from "@/core/utils/uuid";
import { env } from "@/env";
import { cn } from "@/lib/utils";
export default function ChatPage() {
@@ -176,12 +177,18 @@ export default function ChatPage() {
status={thread.isLoading ? "streaming" : "ready"}
context={settings.context}
extraHeader={isNewThread && <Welcome />}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onContextChange={(context) =>
setSettings("context", context)
}
onSubmit={handleSubmit}
onStop={handleStop}
/>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
<div className="text-muted-foreground/67 w-full -translate-y-2 text-center text-xs">
{t.common.notAvailableInDemoMode}
</div>
)}
</div>
</div>
</main>

View File

@@ -51,9 +51,11 @@ export default function ChatsPage() {
<div>
<div>{titleOfThread(thread)}</div>
</div>
<div className="text-muted-foreground text-sm">
{formatTimeAgo(thread.updated_at)}
</div>
{thread.updated_at && (
<div className="text-muted-foreground text-sm">
{formatTimeAgo(thread.updated_at)}
</div>
)}
</div>
</Link>
))}

View File

@@ -1,5 +1,20 @@
import fs from "fs";
import path from "path";
import { redirect } from "next/navigation";
import { env } from "@/env";
export default function WorkspacePage() {
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") {
const firstThread = fs
.readdirSync(path.resolve(process.cwd(), "public/demo/threads"), {
withFileTypes: true,
})
.find((thread) => thread.isDirectory() && !thread.name.startsWith("."));
if (firstThread) {
return redirect(`/workspace/chats/${firstThread.name}`);
}
}
return redirect("/workspace/chats/new");
}

View File

@@ -438,6 +438,7 @@ export type PromptInputProps = Omit<
"onSubmit" | "onError"
> & {
accept?: string; // e.g., "image/*" or leave undefined for any
disabled?: boolean;
multiple?: boolean;
// When true, accepts drops anywhere on document. Default false (opt-in).
globalDrop?: boolean;
@@ -459,6 +460,7 @@ export type PromptInputProps = Omit<
export const PromptInput = ({
className,
accept,
disabled,
multiple,
globalDrop,
syncHiddenInput,

View File

@@ -28,7 +28,7 @@ export function Hero({ className }: { className?: string }) {
/>
</div>
<FlickeringGrid
className="absolute inset-0 z-0 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
className="absolute inset-0 z-0 translate-y-8 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
squareSize={4}
gridGap={4}
color={"white"}

View File

@@ -9,10 +9,13 @@ import {
Sparkles,
Terminal,
Play,
Pause,
} from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { useState, useEffect, useRef } from "react";
import { Tooltip } from "@/components/workspace/tooltip";
type AnimationPhase =
| "idle"
| "user-input"
@@ -69,13 +72,19 @@ export default function ProgressiveSkillsAnimation() {
const [hasAutoPlayed, setHasAutoPlayed] = useState(false);
const chatMessagesRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const timeoutsRef = useRef<NodeJS.Timeout[]>([]);
// Additional display duration after the final step (done) completes, used to show the final result
const FINAL_DISPLAY_DURATION = 3000; // milliseconds
// Play animation only when isPlaying is true
useEffect(() => {
if (!isPlaying) return;
if (!isPlaying) {
// Clear all timeouts when paused
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
return;
}
const timeline = [
{ phase: "user-input" as const, delay: ANIMATION_DELAYS["user-input"] },
@@ -117,7 +126,12 @@ export default function ProgressiveSkillsAnimation() {
}, totalDelay + FINAL_DISPLAY_DURATION),
);
return () => timeouts.forEach(clearTimeout);
timeoutsRef.current = timeouts;
return () => {
timeouts.forEach(clearTimeout);
timeoutsRef.current = [];
};
}, [isPlaying]);
const handlePlay = () => {
@@ -130,6 +144,20 @@ export default function ProgressiveSkillsAnimation() {
setShowWorkspace(false);
};
const handleTogglePlayPause = () => {
if (isPlaying) {
setIsPlaying(false);
} else {
// If animation hasn't started or is at idle, restart from beginning
if (phase === "idle") {
handlePlay();
} else {
// Resume from current phase
setIsPlaying(true);
}
}
};
// Auto-play when component enters viewport for the first time
useEffect(() => {
if (hasAutoPlayed || !containerRef.current) return;
@@ -308,7 +336,7 @@ export default function ProgressiveSkillsAnimation() {
>
{/* Overlay and Play Button */}
<AnimatePresence>
{!isPlaying && (
{!isPlaying && !hasPlayed && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -330,13 +358,30 @@ export default function ProgressiveSkillsAnimation() {
/>
</div>
<span className="text-lg font-medium text-white">
{hasPlayed ? "Click to replay" : "Click to play"}
Click to play
</span>
</motion.button>
</motion.div>
)}
</AnimatePresence>
{/* Bottom Left Play/Pause Button */}
<Tooltip content="Play / Pause">
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
onClick={handleTogglePlayPause}
className="absolute bottom-8 left-8 z-40 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 backdrop-blur-md transition-all hover:scale-110 hover:bg-white/20 active:scale-95"
aria-label={isPlaying ? "暂停" : "播放"}
>
{isPlaying ? (
<Pause size={24} className="text-white" fill="white" />
) : (
<Play size={24} className="ml-0.5 text-white" fill="white" />
)}
</motion.button>
</Tooltip>
<div className="flex h-full max-h-[700px] w-full max-w-6xl gap-8">
{/* Left: File Tree */}
<div className="flex flex-1 flex-col">
@@ -588,7 +633,7 @@ export default function ProgressiveSkillsAnimation() {
className="flex items-center gap-2 text-sm text-green-500"
>
<FileText size={14} />
<span>{file}</span>
<span>Generating {file}...</span>
<Check size={14} />
</motion.div>
))}
@@ -617,7 +662,7 @@ export default function ProgressiveSkillsAnimation() {
className="flex items-center gap-2 pl-4 text-zinc-400"
>
<Terminal size={16} />
<span>Executing deploy.sh</span>
<span>Executing scripts/deploy.sh</span>
</motion.div>
)}
</div>

View File

@@ -12,11 +12,12 @@ export function SandboxSection({ className }: { className?: string }) {
return (
<Section
className={className}
title="Sandbox"
title="Agent Runtime Environment"
subtitle={
<p>
We gave DeerFlow a computer. It can execute code, manage files, and
run long tasks all in a secure Docker sandbox
We give DeerFlow a &quot;computer&quot;, which can execute commands,
manage files, and run long tasks all in a secure Docker-based
sandbox
</p>
}
>

View File

@@ -2,13 +2,13 @@
import { cn } from "@/lib/utils";
import ProgressiveSkillsAnimation from "../components/progressive-skills-animation";
import ProgressiveSkillsAnimation from "../progressive-skills-animation";
import { Section } from "../section";
export function SkillsSection({ className }: { className?: string }) {
return (
<Section
className={cn("h-[calc(100vh-64px)] w-full bg-white/7", className)}
className={cn("h-[calc(100vh-64px)] w-full bg-white/2", className)}
title="Skill-based Architecture"
subtitle={
<div>

View File

@@ -1,10 +1,57 @@
"use client";
import MagicBento from "@/components/ui/magic-bento";
import MagicBento, { type BentoCardProps } from "@/components/ui/magic-bento";
import { cn } from "@/lib/utils";
import { Section } from "../section";
const COLOR = "#0a0a0a";
const features: BentoCardProps[] = [
{
color: COLOR,
label: "Context Engineering",
title: "Long/Short-term Memory",
description: (
<div>
<div>Now the agent can better understand you</div>
<div className="text-muted-foreground">Coming soon</div>
</div>
),
},
{
color: COLOR,
label: "Long Task Running",
title: "Planning and Reasoning",
description: "Plans ahead, reasons through complexity, then acts",
},
{
color: COLOR,
label: "Extensible",
title: "Skills and Tools",
description:
"Plug, play, or even swap built-in tools. Build the agent you want.",
},
{
color: COLOR,
label: "Persistent",
title: "Sandbox with File System",
description: "Read, write, run — like a real computer",
},
{
color: COLOR,
label: "Flexible",
title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
},
{
color: COLOR,
label: "Free",
title: "Open Source",
description: "MIT License, self-hosted, full control",
},
];
export function WhatsNewSection({ className }: { className?: string }) {
return (
<Section
@@ -13,7 +60,7 @@ export function WhatsNewSection({ className }: { className?: string }) {
subtitle="DeerFlow is now evolving from a Deep Research agent into a full-stack Super Agent"
>
<div className="flex w-full items-center justify-center">
<MagicBento />
<MagicBento data={features} />
</div>
</Section>
);

View File

@@ -23,6 +23,7 @@ export interface BentoProps {
glowColor?: string;
clickEffect?: boolean;
enableMagnetism?: boolean;
data: BentoCardProps[];
}
const DEFAULT_PARTICLE_COUNT = 12;
@@ -30,52 +31,6 @@ const DEFAULT_SPOTLIGHT_RADIUS = 300;
const DEFAULT_GLOW_COLOR = "132, 0, 255";
const MOBILE_BREAKPOINT = 768;
const cardData: BentoCardProps[] = [
{
color: "#0a0015",
title: "Long/Short-term Memory",
description: (
<div>
<div>Now the agent can better understand you</div>
<div className="text-muted-foreground">Coming soon</div>
</div>
),
label: "Context Engineering",
},
{
color: "#0a0015",
title: "Planning and Reasoning",
description: "Plans ahead, reasons through complexity, then acts",
label: "Long Task Running",
},
{
color: "#0a0015",
title: "Skills and Tools",
description:
"Plug, play, or even swap built-in tools. Build the agent you want.",
label: "Extensible",
},
{
color: "#0a0015",
title: "Sandbox with File System",
description: "Read, write, run — like a real computer",
label: "Persistent",
},
{
color: "#0a0015",
title: "Multi-Model Support",
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
label: "Flexible",
},
{
color: "#0a0015",
title: "Open Source",
description: "MIT License, self-hosted, full control",
label: "Free",
},
];
const createParticleElement = (
x: number,
y: number,
@@ -571,6 +526,7 @@ const MagicBento: React.FC<BentoProps> = ({
glowColor = DEFAULT_GLOW_COLOR,
clickEffect = true,
enableMagnetism = true,
data: cardData,
}) => {
const gridRef = useRef<HTMLDivElement>(null);
const isMobile = useMobileDetection();

View File

@@ -230,7 +230,7 @@ export const Terminal = ({
<div
ref={containerRef}
className={cn(
"border-border bg-background z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
"border-border bg-background/25 z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
className,
)}
>

View File

@@ -48,6 +48,7 @@ import {
export function InputBox({
className,
disabled,
autoFocus,
status = "ready",
context,
@@ -60,6 +61,7 @@ export function InputBox({
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null;
status?: ChatStatus;
disabled?: boolean;
context: Omit<AgentThreadContext, "thread_id">;
extraHeader?: React.ReactNode;
isNewThread?: boolean;
@@ -142,6 +144,7 @@ export function InputBox({
"bg-background/85 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
className,
)}
disabled={disabled}
globalDrop
multiple
onSubmit={handleSubmit}
@@ -160,6 +163,7 @@ export function InputBox({
<PromptInputBody className="absolute top-0 right-0 left-0 z-3">
<PromptInputTextarea
className={cn("size-full")}
disabled={disabled}
placeholder={t.inputBox.placeholder}
autoFocus={autoFocus}
/>
@@ -303,6 +307,7 @@ export function InputBox({
</ModelSelector>
<PromptInputSubmit
className="rounded-full"
disabled={disabled}
variant="outline"
status={status}
/>

View File

@@ -23,6 +23,7 @@ import {
import { useI18n } from "@/core/i18n/hooks";
import { useDeleteThread, useThreads } from "@/core/threads/hooks";
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
import { env } from "@/env";
export function RecentChatList() {
const { t } = useI18n();
@@ -54,7 +55,11 @@ export function RecentChatList() {
}
return (
<SidebarGroup>
<SidebarGroupLabel>{t.sidebar.recentChats}</SidebarGroupLabel>
<SidebarGroupLabel>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
? t.sidebar.recentChats
: t.sidebar.demoChats}
</SidebarGroupLabel>
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
<SidebarMenu>
<div className="flex w-full flex-col gap-1">
@@ -73,29 +78,31 @@ export function RecentChatList() {
>
{titleOfThread(thread)}
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="bg-background/50 hover:bg-background"
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="bg-background/50 hover:bg-background"
>
<MoreHorizontal />
<span className="sr-only">{t.common.more}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={"right"}
align={"start"}
>
<MoreHorizontal />
<span className="sr-only">{t.common.more}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={"right"}
align={"start"}
>
<DropdownMenuItem
onSelect={() => handleDelete(thread.thread_id)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuItem
onSelect={() => handleDelete(thread.thread_id)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</SidebarMenuButton>
</SidebarMenuItem>

View File

@@ -47,7 +47,6 @@ export function SettingsDialog({
t.settings.sections.skills,
],
);
return (
<Dialog {...dialogProps}>
<DialogContent

View File

@@ -22,6 +22,7 @@ import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useEnableSkill, useSkills } from "@/core/skills/hooks";
import type { Skill } from "@/core/skills/type";
import { env } from "@/env";
import { SettingsSection } from "./settings-section";
@@ -116,6 +117,7 @@ function SkillSettingsList({ skills }: { skills: Skill[] }) {
<ItemActions>
<Switch
checked={skill.enabled}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onCheckedChange={(checked) =>
enableSkill({ skillName: skill.name, enabled: checked })
}

View File

@@ -11,6 +11,7 @@ import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useMCPConfig, useEnableMCPServer } from "@/core/mcp/hooks";
import type { MCPServerConfig } from "@/core/mcp/types";
import { env } from "@/env";
import { SettingsSection } from "./settings-section";
@@ -56,6 +57,7 @@ function MCPServerList({
<ItemActions>
<Switch
checked={config.enabled}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onCheckedChange={(checked) =>
enableMCPServer({ serverName: name, enabled: checked })
}

View File

@@ -12,7 +12,9 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { useI18n } from "@/core/i18n/hooks";
import { env } from "@/env";
import { cn } from "@/lib/utils";
import { Tooltip } from "./tooltip";
export function WorkspaceHeader({ className }: { className?: string }) {
const { t } = useI18n();
@@ -35,7 +37,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
</div>
) : (
<div className="flex items-center justify-between gap-2">
<Link href="/workspace" className="text-primary ml-2 font-serif">
<Link href="/" className="text-primary ml-2 font-serif">
DeerFlow
</Link>
<SidebarTrigger />

View File

@@ -4,8 +4,6 @@ export function getBackendBaseURL() {
if (env.NEXT_PUBLIC_BACKEND_BASE_URL) {
return env.NEXT_PUBLIC_BACKEND_BASE_URL;
} else {
// Use empty string for same-origin requests through nginx
// Paths like /api/models will be handled by nginx proxy
return "";
}
}

View File

@@ -20,6 +20,7 @@ export const enUS: Translations = {
artifacts: "Artifacts",
public: "Public",
custom: "Custom",
notAvailableInDemoMode: "Not available in demo mode",
},
// Welcome
@@ -57,6 +58,7 @@ export const enUS: Translations = {
newChat: "New chat",
chats: "Chats",
recentChats: "Recent chats",
demoChats: "Demo chats",
},
// Breadcrumb

View File

@@ -18,6 +18,7 @@ export interface Translations {
artifacts: string;
public: string;
custom: string;
notAvailableInDemoMode: string;
};
// Welcome
@@ -52,6 +53,7 @@ export interface Translations {
recentChats: string;
newChat: string;
chats: string;
demoChats: string;
};
// Breadcrumb

View File

@@ -20,6 +20,7 @@ export const zhCN: Translations = {
artifacts: "文件",
public: "公共",
custom: "自定义",
notAvailableInDemoMode: "在演示模式下不可用",
},
// Welcome
@@ -54,7 +55,8 @@ export const zhCN: Translations = {
sidebar: {
newChat: "新对话",
chats: "对话",
recentChats: "最近的聊天",
recentChats: "最近的对话",
demoChats: "演示对话",
},
// Breadcrumb