feat: 初步项目结构

This commit is contained in:
grtsinry43 2025-04-25 17:00:24 +08:00
parent 9ae820a796
commit 37567f4ef8
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
14 changed files with 1678 additions and 295 deletions

View File

@ -0,0 +1,337 @@
<script>
import { onMount } from "svelte";
import { Toaster, createToaster } from "@skeletonlabs/skeleton-svelte";
import { Scale, Calendar, ArrowDownWideNarrow, Grid, List, HardDrive, Clock } from "lucide-svelte";
// 接收服务端渲染的初始数据
export let initialMirrors = [];
export let initialError = null;
export let categories = [];
// 状态变量
let mirrors = initialMirrors;
let loading = initialMirrors.length === 0 && !initialError;
let error = initialError;
let searchTerm = "";
let sortBy = "name"; // 默认按名称排序
let selectedCategory = "all"; // 默认显示所有类别
let viewMode = "grid"; // 默认网格视图 ('grid' 或 'list')
// Toast通知
const toaster = createToaster();
// 获取镜像数据
async function fetchMirrors() {
try {
loading = true;
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
mirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
// 添加示例分类
category: determineCategory(name),
}));
error = null;
} catch (err) {
error = err.message;
console.error(err);
toaster.error({
title: "加载失败",
description: `无法获取镜像列表: ${err.message}`,
timeout: 5000
});
} finally {
loading = false;
}
}
// 根据名称确定类别的辅助函数(示例实现)
function determineCategory(name) {
if (/ubuntu|debian|fedora|centos|arch|opensuse/i.test(name)) return "Linux发行版";
if (/python|npm|maven|gradle|rust|cargo/i.test(name)) return "开发工具";
if (/apache|nginx|tomcat|docker|kubernetes/i.test(name)) return "服务器软件";
if (/gcc|llvm|clang|make|cmake/i.test(name)) return "编译工具";
if (/mysql|postgresql|mongodb|redis|elasticsearch/i.test(name)) return "数据库";
if (/anaconda|tensorflow|pytorch|scikit/i.test(name)) return "数据科学";
if (/kernel|core|linux/i.test(name)) return "核心组件";
if (/ubuntu|windows|ios|android/i.test(name)) return "操作系统";
return "其他镜像";
}
// 获取镜像图标(简单实现,实际可能需要更复杂的逻辑或外部图标库)
function getMirrorIcon(name) {
if (/ubuntu/i.test(name)) return "🐧";
if (/debian/i.test(name)) return "🔴";
if (/fedora/i.test(name)) return "🔵";
if (/windows/i.test(name)) return "🪟";
if (/python/i.test(name)) return "🐍";
if (/anaconda/i.test(name)) return "🐍";
if (/npm|node/i.test(name)) return "📦";
if (/docker/i.test(name)) return "🐳";
if (/database|mysql|postgres|mongo/i.test(name)) return "🗄️";
if (/maven|gradle/i.test(name)) return "☕";
return "📁";
}
// 排序和过滤
$: {
let filtered = [...mirrors];
// 应用类别过滤
if (selectedCategory !== "all") {
filtered = filtered.filter(m => m.category === selectedCategory);
}
// 应用搜索过滤
if (searchTerm) {
filtered = filtered.filter(m =>
m.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.category.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// 应用排序
if (sortBy === "name") {
filtered.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortBy === "status") {
filtered.sort((a, b) => a.status.localeCompare(b.status));
} else if (sortBy === "date") {
filtered.sort((a, b) => b.last_update_ts - a.last_update_ts);
} else if (sortBy === "size") {
filtered.sort((a, b) => {
// 简单的大小比较,实际实现可能更复杂
const sizeA = a.size || '0';
const sizeB = b.size || '0';
return sizeB.localeCompare(sizeA);
});
}
filteredMirrors = filtered;
}
// 过滤后的镜像
let filteredMirrors = [];
// 获取状态对应的样式
function getStatusBadgeClass(status) {
switch (status) {
case "success": return "badge-success";
case "failed": return "badge-error";
case "syncing": return "badge-warning";
default: return "badge-surface";
}
}
// 格式化状态文本
function formatStatus(status) {
switch (status) {
case "success": return "正常";
case "failed": return "失败";
case "syncing": return "同步中";
default: return status;
}
}
onMount(() => {
// 如果没有初始数据或有错误时,才在客户端请求数据
if (initialMirrors.length === 0 && !initialError) {
fetchMirrors();
}
});
</script>
<div class="container mx-auto">
<div class="card p-4">
<!-- 过滤和控制区域 -->
<div class="flex flex-col gap-4 mb-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<!-- 搜索框 -->
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
<div class="input-group-shim">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
</div>
<input bind:value={searchTerm} type="search" placeholder="搜索镜像..." class="input p-2" />
</div>
<!-- 类别过滤 -->
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
<div class="input-group-shim">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-filter" viewBox="0 0 16 16">
<path d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
</svg>
</div>
<select bind:value={selectedCategory} class="select p-2">
<option value="all">全部类别</option>
{#each categories as category}
<option value={category}>{category}</option>
{/each}
</select>
</div>
<!-- 排序选项 -->
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
<div class="input-group-shim">
<ArrowDownWideNarrow size={16} />
</div>
<select bind:value={sortBy} class="select p-2">
<option value="name">按名称排序</option>
<option value="status">按状态排序</option>
<option value="date">按更新时间排序</option>
<option value="size">按大小排序</option>
</select>
</div>
</div>
<!-- 视图切换和刷新按钮 -->
<div class="flex justify-between items-center">
<div class="btn-group variant-filled-surface">
<button class={`btn ${viewMode === 'grid' ? 'variant-filled-primary' : ''}`} on:click={() => viewMode = 'grid'}>
<Grid size={18} />
<span class="hidden md:inline ml-1">网格视图</span>
</button>
<button class={`btn ${viewMode === 'list' ? 'variant-filled-primary' : ''}`} on:click={() => viewMode = 'list'}>
<List size={18} />
<span class="hidden md:inline ml-1">列表视图</span>
</button>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-surface-600-300-token">{filteredMirrors.length} 个镜像</span>
<button class="btn variant-filled-primary" on:click={fetchMirrors}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
<span class="ml-1">刷新</span>
</button>
</div>
</div>
</div>
<!-- 加载状态 -->
{#if loading}
<div class="flex justify-center p-10">
<div class="loader h-10 w-10"></div>
</div>
<!-- 错误信息 -->
{:else if error}
<div class="alert variant-filled-error">
<p>加载失败: {error}</p>
<button class="btn variant-filled" on:click={fetchMirrors}>重试</button>
</div>
<!-- 数据显示 - 网格视图 -->
{:else if viewMode === 'grid'}
{#if filteredMirrors.length > 0}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{#each filteredMirrors as mirror}
<a href={`/mirrors/${mirror.name}`} class="card p-4 hover:bg-surface-200-700-token transition-colors duration-200">
<header class="flex items-center gap-3 mb-3">
<div class="text-2xl">{getMirrorIcon(mirror.name)}</div>
<h3 class="h3 line-clamp-1">{mirror.name}</h3>
</header>
<div class="space-y-2">
<div class="flex justify-between">
<span class="badge {getStatusBadgeClass(mirror.status)}">{formatStatus(mirror.status)}</span>
<span class="badge variant-soft">{mirror.category}</span>
</div>
<div class="flex items-center gap-2 text-sm text-surface-600-300-token">
<HardDrive size={14} />
<span>{mirror.size || 'N/A'}</span>
</div>
<div class="flex items-center gap-2 text-sm text-surface-600-300-token">
<Clock size={14} />
<span>{mirror.lastUpdated}</span>
</div>
</div>
</a>
{/each}
</div>
{:else}
<div class="flex flex-col items-center justify-center p-10 space-y-4">
<div class="text-6xl">🔍</div>
<p class="text-xl text-center text-surface-600-300-token">未找到符合条件的镜像</p>
<button class="btn variant-filled-primary" on:click={() => {searchTerm = ''; selectedCategory = 'all';}}>
清除过滤条件
</button>
</div>
{/if}
<!-- 数据显示 - 列表视图 -->
{:else}
{#if filteredMirrors.length > 0}
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th>镜像名称</th>
<th>类别</th>
<th>状态</th>
<th>大小</th>
<th>最后更新时间</th>
</tr>
</thead>
<tbody>
{#each filteredMirrors as mirror}
<tr
class="{mirror.status === 'success' ? '' :
mirror.status === 'failed' ? 'bg-red-500/10' :
mirror.status === 'syncing' ? 'bg-yellow-500/10' : ''} hover:bg-surface-200/50 transition-colors duration-300"
on:click={() => window.location.href = `/mirrors/${mirror.name}`}
>
<td class="flex items-center gap-2">
<span class="text-xl">{getMirrorIcon(mirror.name)}</span>
<span class="space-grotesk-google">{mirror.name}</span>
</td>
<td><span class="badge variant-soft">{mirror.category}</span></td>
<td>
<span class="badge {getStatusBadgeClass(mirror.status)}">
{formatStatus(mirror.status)}
</span>
</td>
<td><span class="space-grotesk-google">{mirror.size || 'N/A'}</span></td>
<td><span class="space-grotesk-google">{mirror.lastUpdated}</span></td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<div class="flex flex-col items-center justify-center p-10 space-y-4">
<div class="text-6xl">🔍</div>
<p class="text-xl text-center text-surface-600-300-token">未找到符合条件的镜像</p>
<button class="btn variant-filled-primary" on:click={() => {searchTerm = ''; selectedCategory = 'all';}}>
清除过滤条件
</button>
</div>
{/if}
{/if}
</div>
</div>
<style>
/* 美化卡片动画效果 */
a.card {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
a.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* 表格行动画效果 */
tr {
cursor: pointer;
}
</style>
<Toaster {toaster}></Toaster>

View File

@ -1,6 +1,7 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Toaster, createToaster } from "@skeletonlabs/skeleton-svelte"; import { Toaster, createToaster } from "@skeletonlabs/skeleton-svelte";
import { fade, fly } from "svelte/transition";
// 导入Lucide图标 // 导入Lucide图标
import { Search, Moon, Sun, Menu, X } from "lucide-svelte"; import { Search, Moon, Sun, Menu, X } from "lucide-svelte";
@ -9,6 +10,7 @@
let darkMode = false; let darkMode = false;
let mobileMenuOpen = false; let mobileMenuOpen = false;
let searchQuery = ""; let searchQuery = "";
let isScrolled = false;
// Toast通知 // Toast通知
const toaster = createToaster(); const toaster = createToaster();
@ -16,12 +18,18 @@
// 切换主题 // 切换主题
function toggleTheme() { function toggleTheme() {
darkMode = !darkMode; darkMode = !darkMode;
document.documentElement.classList.toggle("dark", darkMode);
// // 保存用户偏好 // 更新 HTML 类和属性
// if (typeof localStorage !== "undefined") { document.documentElement.classList.toggle("dark", darkMode);
// localStorage.setItem("theme", darkMode ? "dark" : "light"); document.documentElement.setAttribute(
// } "data-theme",
darkMode ? "skeleton-dark" : "skeleton"
);
// 保存用户偏好
if (typeof localStorage !== "undefined") {
localStorage.setItem("theme", darkMode ? "dark" : "light");
}
// 显示主题切换通知 // 显示主题切换通知
toaster.info({ toaster.info({
@ -48,38 +56,59 @@
} }
} }
// // 在组件挂载时检查用户的主题偏好 // 监听滚动事件
// onMount(() => { function handleScroll() {
// if (typeof localStorage !== "undefined" && localStorage.getItem("theme") === "dark") { isScrolled = window.scrollY > 10;
// darkMode = true; }
// document.documentElement.classList.add("dark");
// } else if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) { // 在组件挂载时检查用户的主题偏好和设置滚动监听
// darkMode = true; onMount(() => {
// document.documentElement.classList.add("dark"); // 主题设置
// } if (typeof localStorage !== "undefined" && localStorage.getItem("theme") === "dark") {
// }); darkMode = true;
document.documentElement.classList.add("dark");
document.documentElement.setAttribute("data-theme", "skeleton-dark");
} else if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
darkMode = true;
document.documentElement.classList.add("dark");
document.documentElement.setAttribute("data-theme", "skeleton-dark");
}
// 滚动监听
window.addEventListener("scroll", handleScroll);
// 组件卸载时移除监听器
return () => {
window.removeEventListener("scroll", handleScroll);
};
});
</script> </script>
<header <header
class="sticky top-0 z-50 w-full transition-all duration-300 bg-background/80 backdrop-blur-2xl border-b border-surface-300-600-token shadow-md"> class="sticky top-0 z-50 w-full transition-all duration-300 {isScrolled ? 'shadow-lg' : 'shadow-md'}
{isScrolled ? 'bg-surface-100-800-token/95' : 'bg-surface-100-800-token/80'}
backdrop-blur-2xl border-b border-surface-300-600-token">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16">
<!-- 网站Logo和标题 --> <!-- 网站Logo和标题 -->
<div class="flex items-center"> <div class="flex items-center">
<a href="/" class="flex items-center space-x-2"> <a href="/" class="flex items-center space-x-2 group">
<div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center text-white font-bold">M</div> <div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center text-white font-bold
transition-all duration-300 group-hover:scale-110">M
</div>
<span <span
class="text-xl font-bold tracking-wider bg-gradient-to-r from-primary-500 to-tertiary-500 bg-clip-text text-transparent">我的镜像站</span> class="text-xl font-bold tracking-wider bg-gradient-to-r from-primary-500 to-tertiary-500 bg-clip-text text-transparent
transition-all duration-300 group-hover:from-tertiary-500 group-hover:to-primary-500">我的镜像站</span>
</a> </a>
</div> </div>
<!-- 桌面导航 --> <!-- 桌面导航 -->
<nav class="hidden md:flex items-center space-x-1"> <nav class="hidden md:flex items-center space-x-1">
<a href="/" class="btn btn-sm variant-ghost-surface">首页</a> <a href="/" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">首页</a>
<a href="/browse" class="btn btn-sm variant-ghost-surface">浏览</a> <a href="/browse" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">浏览</a>
<a href="/popular" class="btn btn-sm variant-ghost-surface">热门</a> <a href="/popular" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">热门</a>
<a href="/new" class="btn btn-sm variant-ghost-surface">最新</a> <a href="/new" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">最新</a>
<a href="/about" class="btn btn-sm variant-ghost-surface">关于</a> <a href="/about" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">关于</a>
</nav> </nav>
<!-- 搜索和主题切换 --> <!-- 搜索和主题切换 -->
@ -91,10 +120,10 @@
bind:value={searchQuery} bind:value={searchQuery}
on:keydown={handleSearch} on:keydown={handleSearch}
placeholder="搜索镜像..." placeholder="搜索镜像..."
class="input input-sm pl-9 pr-4 rounded-full w-40 lg:w-64 focus:w-72 transition-all duration-300" class="input input-sm pl-9 pr-4 rounded-full w-40 lg:w-64 focus:w-72 transition-all duration-300 focus:ring-primary-500"
/> />
<button <button
class="absolute left-2 top-1/2 transform -translate-y-1/2 text-surface-500-400-token" class="absolute left-2 top-1/2 transform -translate-y-1/2 text-surface-500-400-token hover:text-primary-500 transition-colors"
on:click={handleSearch} on:click={handleSearch}
> >
<Search size={18} /> <Search size={18} />
@ -103,22 +132,24 @@
<!-- 主题切换 --> <!-- 主题切换 -->
<button <button
class="btn btn-sm variant-ghost-surface aspect-square" class="btn btn-sm variant-ghost-surface aspect-square hover:variant-soft-primary"
on:click={toggleTheme} on:click={toggleTheme}
aria-label="切换主题" aria-label="切换主题"
title={darkMode ? "切换至亮色模式" : "切换至暗色模式"}
> >
{#if darkMode} {#if darkMode}
<Sun size={18} /> <Sun size={18} class="transition-transform hover:rotate-45 duration-300" />
{:else} {:else}
<Moon size={18} /> <Moon size={18} class="transition-transform hover:rotate-12 duration-300" />
{/if} {/if}
</button> </button>
<!-- 移动端菜单按钮 --> <!-- 移动端菜单按钮 -->
<button <button
class="btn btn-sm variant-ghost-surface md:hidden aspect-square" class="btn btn-sm variant-ghost-surface md:hidden aspect-square hover:variant-soft-primary"
on:click={() => mobileMenuOpen = !mobileMenuOpen} on:click={() => mobileMenuOpen = !mobileMenuOpen}
aria-label="菜单" aria-label="菜单"
aria-expanded={mobileMenuOpen}
> >
{#if mobileMenuOpen} {#if mobileMenuOpen}
<X size={18} /> <X size={18} />
@ -133,13 +164,17 @@
<!-- 移动端菜单 --> <!-- 移动端菜单 -->
{#if mobileMenuOpen} {#if mobileMenuOpen}
<div class="md:hidden bg-surface-100-800-token border-t border-surface-300-600-token"> <div class="md:hidden bg-surface-100-800-token border-t border-surface-300-600-token"
transition:fly={{ y: -10, duration: 200 }}>
<div class="container mx-auto px-4 py-3 space-y-2"> <div class="container mx-auto px-4 py-3 space-y-2">
<a href="/" class="btn btn-sm w-full variant-ghost-surface justify-start">首页</a> <a href="/" class="btn btn-sm w-full variant-ghost-surface hover:variant-soft-primary justify-start">首页</a>
<a href="/browse" class="btn btn-sm w-full variant-ghost-surface justify-start">浏览</a> <a href="/browse"
<a href="/popular" class="btn btn-sm w-full variant-ghost-surface justify-start">热门</a> class="btn btn-sm w-full variant-ghost-surface hover:variant-soft-primary justify-start">浏览</a>
<a href="/new" class="btn btn-sm w-full variant-ghost-surface justify-start">最新</a> <a href="/popular"
<a href="/about" class="btn btn-sm w-full variant-ghost-surface justify-start">关于</a> class="btn btn-sm w-full variant-ghost-surface hover:variant-soft-primary justify-start">热门</a>
<a href="/new" class="btn btn-sm w-full variant-ghost-surface hover:variant-soft-primary justify-start">最新</a>
<a href="/about"
class="btn btn-sm w-full variant-ghost-surface hover:variant-soft-primary justify-start">关于</a>
<!-- 移动端搜索框 --> <!-- 移动端搜索框 -->
<div class="relative w-full mt-2"> <div class="relative w-full mt-2">
@ -148,10 +183,10 @@
bind:value={searchQuery} bind:value={searchQuery}
on:keydown={handleSearch} on:keydown={handleSearch}
placeholder="搜索镜像..." placeholder="搜索镜像..."
class="input input-sm pl-9 pr-4 rounded-full w-full" class="input input-sm pl-9 pr-4 rounded-full w-full focus:ring-primary-500"
/> />
<button <button
class="absolute left-2 top-1/2 transform -translate-y-1/2 text-surface-500-400-token" class="absolute left-2 top-1/2 transform -translate-y-1/2 text-surface-500-400-token hover:text-primary-500 transition-colors"
on:click={handleSearch} on:click={handleSearch}
> >
<Search size={18} /> <Search size={18} />
@ -163,18 +198,18 @@
</header> </header>
<style> <style>
/*!* 活动链接样式 *!*/ /* 活动链接样式 */
/*:global(a.active) {*/ :global(a.active) {
/*@apply bg-primary-500/20 text-primary-500;*/ /*@apply bg-primary-500/20 text-primary-500;*/
/*}*/ }
/*!* 链接悬停效果 *!*/ /* 链接悬停效果 */
/*a.btn:hover {*/ a.btn:hover {
/*@apply bg-primary-500/10;*/ /*@apply bg-primary-500/10;*/
/*}*/ }
/*!* 平滑过渡 *!*/ /* 平滑过渡 */
/*a, button {*/ a, button {
/*@apply transition-all duration-200;*/ /*@apply transition-all duration-200;*/
/*}*/ }
</style> </style>

View File

@ -2,10 +2,14 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Toaster, createToaster } from "@skeletonlabs/skeleton-svelte"; import { Toaster, createToaster } from "@skeletonlabs/skeleton-svelte";
// 接收服务端渲染的初始数据
export let initialMirrors = [];
export let initialError = null;
// 状态变量 // 状态变量
let mirrors = []; let mirrors = initialMirrors;
let loading = true; let loading = initialMirrors.length === 0 && !initialError;
let error = null; let error = initialError;
let searchTerm = ""; let searchTerm = "";
let sortBy = "name"; // 默认按名称排序 let sortBy = "name"; // 默认按名称排序
@ -31,7 +35,7 @@
// 格式化最后更新时间 // 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN") lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN")
})); }));
error = null;
} catch (err) { } catch (err) {
error = err.message; error = err.message;
console.error(err); console.error(err);
@ -72,9 +76,17 @@
} }
} }
// 跳转到镜像详情页
function goToMirrorPage(mirrorName) {
window.location.href = `/mirror/${mirrorName}`;
}
onMount(() => { onMount(() => {
// 如果没有初始数据或有错误时,才在客户端请求数据
if (initialMirrors.length === 0 && !initialError) {
fetchMirrors(); fetchMirrors();
console.log("Mounted and fetching mirrors..."); }
console.log("Component mounted with initial data:", initialMirrors.length);
}); });
</script> </script>
@ -134,7 +146,8 @@
{#each filteredMirrors as mirror (mirror.name)} {#each filteredMirrors as mirror (mirror.name)}
<tr class="{mirror.status === 'success' ? '' : <tr class="{mirror.status === 'success' ? '' :
mirror.status === 'failed' ? 'bg-red-500/10' : mirror.status === 'failed' ? 'bg-red-500/10' :
mirror.status === 'syncing' ? 'bg-yellow-500/10' : ''} hover:bg-surface-200/50 transition-colors duration-300 cursor-pointer"> mirror.status === 'syncing' ? 'bg-yellow-500/10' : ''} hover:bg-surface-200/50 transition-colors duration-300 cursor-pointer"
on:click={() => goToMirrorPage(mirror.name)}>
<td><span class="space-grotesk-google">{mirror.name}</span></td> <td><span class="space-grotesk-google">{mirror.name}</span></td>
<td> <td>
<span class="badge {getStatusBadgeClass(mirror.status)}"> <span class="badge {getStatusBadgeClass(mirror.status)}">

View File

@ -0,0 +1,334 @@
<script>
import { Tabs, } from "@skeletonlabs/skeleton-svelte";
// 接收镜像数据作为属性
export let mirror;
// 复制代码到剪贴板的功能
let copiedCode = "";
let copyTimeout;
function copyToClipboard(code) {
navigator.clipboard.writeText(code)
.then(() => {
copiedCode = code;
if (copyTimeout) clearTimeout(copyTimeout);
copyTimeout = setTimeout(() => {
copiedCode = "";
}, 3000);
})
.catch(err => {
console.error("无法复制代码: ", err);
});
}
// 教程数据 - 在生产环境中应来自API或CMS
const tutorialData = {
// Ubuntu 镜像的教程
ubuntu: {
title: "Ubuntu 镜像使用帮助",
description: "Ubuntu 是一个基于 Debian 的 Linux 发行版,具有良好的社区支持和丰富的软件包。",
tabs: [
{
id: "apt-sources",
label: "修改软件源",
content: `
<h3 class="h3">修改软件源</h3>
<p>Ubuntu 的软件源配置文件是 <code>/etc/apt/sources.list</code>。将系统自带的该文件做个备份,再替换为下面内容:</p>
<div class="code-block">
<pre><code>deb http://mirrors.example.org/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.example.org/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.example.org/ubuntu/ focal-backports main restricted universe multiverse
deb http://mirrors.example.org/ubuntu/ focal-security main restricted universe multiverse</code></pre>
<button class="copy-button">复制</button>
</div>
<p>然后执行更新:</p>
<div class="code-block">
<pre><code>sudo apt update</code></pre>
<button class="copy-button">复制</button>
</div>
`
},
{
id: "iso-download",
label: "ISO 镜像下载",
content: `
<h3 class="h3">ISO 镜像下载</h3>
<p>Ubuntu 发行版 ISO 镜像可以在以下位置找到:</p>
<ul class="list-disc ml-6 mb-4">
<li><a href="#" class="anchor">Ubuntu 22.04 LTS (Jammy Jellyfish)</a></li>
<li><a href="#" class="anchor">Ubuntu 20.04 LTS (Focal Fossa)</a></li>
<li><a href="#" class="anchor">Ubuntu 18.04 LTS (Bionic Beaver)</a></li>
</ul>
<p>推荐使用 LTS (长期支持) 版本以获取更稳定的体验。</p>
`
},
{
id: "faq",
label: "常见问题",
content: `
<h3 class="h3">常见问题</h3>
<div class="card p-4 mb-4">
<h4 class="h4">无法更新软件源?</h4>
<p>可能是由于网络问题或者软件源配置错误导致的。请检查您的网络连接,确保软件源地址正确。</p>
</div>
<div class="card p-4 mb-4">
<h4 class="h4">如何切换回官方源?</h4>
<p>您可以通过图形界面的"软件与更新"工具选择官方源,或者手动编辑 sources.list 文件恢复备份。</p>
</div>
<div class="card p-4">
<h4 class="h4">为什么下载速度较慢?</h4>
<p>下载速度受多种因素影响,包括当前的网络环境、服务器负载等。您可以尝试在不同时段下载或者切换到其他镜像源。</p>
</div>
`
}
]
},
// Debian 镜像的教程
debian: {
title: "Debian 镜像使用帮助",
description: "Debian 是一个自由的操作系统,由全球众多志愿者共同维护。",
tabs: [
{
id: "apt-sources",
label: "修改软件源",
content: `
<h3 class="h3">修改软件源</h3>
<p>Debian 的软件源配置文件是 <code>/etc/apt/sources.list</code>。将系统自带的该文件做个备份,再替换为下面内容:</p>
<div class="code-block">
<pre><code>deb http://mirrors.example.org/debian/ bullseye main contrib non-free
deb http://mirrors.example.org/debian/ bullseye-updates main contrib non-free
deb http://mirrors.example.org/debian/ bullseye-backports main contrib non-free
deb http://security.debian.org/debian-security bullseye-security main contrib non-free</code></pre>
<button class="copy-button">复制</button>
</div>
<p>然后执行更新:</p>
<div class="code-block">
<pre><code>sudo apt update</code></pre>
<button class="copy-button">复制</button>
</div>
`
},
{
id: "iso-download",
label: "ISO 镜像下载",
content: `
<h3 class="h3">ISO 镜像下载</h3>
<p>Debian 发行版 ISO 镜像可以在以下位置找到:</p>
<ul class="list-disc ml-6 mb-4">
<li><a href="#" class="anchor">Debian 11 (Bullseye)</a></li>
<li><a href="#" class="anchor">Debian 10 (Buster)</a></li>
<li><a href="#" class="anchor">Debian 9 (Stretch)</a></li>
</ul>
`
},
{
id: "faq",
label: "常见问题",
content: `
<h3 class="h3">常见问题</h3>
<div class="card p-4 mb-4">
<h4 class="h4">non-free 和 contrib 是什么?</h4>
<p>Debian 将软件包分为 main、contrib 和 non-free 三个部分。main 包含符合 Debian 自由软件指导方针的软件包contrib 包含的软件虽然是自由的但它依赖于非自由软件non-free 包含的软件不符合 Debian 自由软件指导方针。</p>
</div>
`
}
]
},
// 默认通用教程模板
default: {
title: "镜像使用帮助",
description: "此镜像源提供了各种软件包和资源的下载服务。",
tabs: [
{
id: "usage",
label: "使用方法",
content: `
<h3 class="h3">使用方法</h3>
<p>请根据不同操作系统和软件的具体情况,参考下面的指南配置您的软件源:</p>
<div class="card p-4 mb-4">
<h4 class="h4"><Terminal size={20} class="inline-block mr-1" />命令行配置</h4>
<p class="mb-2">您可以通过命令行工具配置此镜像源:</p>
<div class="code-block">
<pre><code># 替换为适合您系统的命令
echo "source_url=http://mirrors.example.org/${mirror?.name}" >> ~/.config/mirror.conf</code></pre>
<button class="copy-button">复制</button>
</div>
</div>
<div class="card p-4">
<h4 class="h4">图形界面配置</h4>
<p>在软件的设置页面中,找到"镜像源"或"软件源"设置,输入以下地址:</p>
<div class="code-block">
<pre><code>http://mirrors.example.org/${mirror?.name}</code></pre>
<button class="copy-button">复制</button>
</div>
</div>
`
},
{
id: "faq",
label: "常见问题",
content: `
<h3 class="h3">常见问题</h3>
<div class="card p-4 mb-4">
<h4 class="h4">如何检查镜像是否可用?</h4>
<p>您可以通过访问镜像站首页或者尝试使用 ping 命令检测连通性:</p>
<div class="code-block">
<pre><code>ping mirrors.example.org</code></pre>
<button class="copy-button">复制</button>
</div>
</div>
<div class="card p-4">
<h4 class="h4">遇到问题如何寻求帮助?</h4>
<p>如果您在使用过程中遇到问题,可以通过以下方式寻求帮助:</p>
<ul class="list-disc ml-6">
<li>查看镜像站的常见问题解答</li>
<li>在官方社区论坛发帖</li>
<li>联系我们的技术支持团队</li>
</ul>
</div>
`
}
]
}
};
// 根据镜像名称选择对应教程,如果没有特定教程则使用默认模板
$: tutorial = tutorialData[mirror.name.toLowerCase()] || tutorialData.default;
// 自动替换内容中的变量
$: processedTabs = tutorial.tabs.map(tab => ({
...tab,
content: tab.content.replace(/\${mirror\?.name}/g, mirror.name)
}));
// 更新时间
$: lastUpdateDate = new Date(mirror.last_update_ts * 1000);
$: lastUpdateFormatted = lastUpdateDate.toLocaleString("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
</script>
<div class="card p-6 variant-glass">
<header class="mb-6">
<h1 class="h2 mb-3">{mirror.name} <span
class="badge {mirror.status === 'success' ? 'badge-success' : mirror.status === 'failed' ? 'badge-error' : 'badge-warning'}">{mirror.status === 'success' ? '正常' : mirror.status === 'failed' ? '失败' : '同步中'}</span>
</h1>
<p class="text-lg">{tutorial.description}</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
<div class="card p-3 flex items-center gap-3">
<div class="bg-primary-500/20 p-2 rounded-token">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-primary-500"
viewBox="0 0 16 16">
<path
d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z" />
<path
d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z" />
</svg>
</div>
<div>
<h3 class="text-base font-medium">状态</h3>
<p>{mirror.status === 'success' ? '正常运行中' : mirror.status === 'failed' ? '同步失败' : '正在同步'}</p>
</div>
</div>
<div class="card p-3 flex items-center gap-3">
<div class="bg-tertiary-500/20 p-2 rounded-token">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-tertiary-500"
viewBox="0 0 16 16">
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z" />
</svg>
</div>
<div>
<h3 class="text-base font-medium">最后更新</h3>
<p>{lastUpdateFormatted}</p>
</div>
</div>
<div class="card p-3 flex items-center gap-3">
<div class="bg-secondary-500/20 p-2 rounded-token">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="text-secondary-500"
viewBox="0 0 16 16">
<path
d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z" />
</svg>
</div>
<div>
<h3 class="text-base font-medium">镜像大小</h3>
<p>{mirror.size || '暂无数据'}</p>
</div>
</div>
</div>
</header>
<Tabs.Group>
{#each processedTabs as tab, i}
<Tabs value={i}>{tab.label}</Tabs>
{/each}
{#each processedTabs as tab, i}
<Tabs.Panel name={`panel-${i}`} class="guide-content">
{@html tab.content}
</Tabs.Panel>
{/each}
</Tabs.Group>
</div>
<style>
.guide-content :global(h3) {
margin-bottom: 1rem;
}
.guide-content :global(p) {
margin-bottom: 1rem;
}
.guide-content :global(.code-block) {
position: relative;
margin-bottom: 1.5rem;
background-color: var(--color-surface-900);
border-radius: var(--theme-rounded-base);
overflow: hidden;
}
.guide-content :global(pre) {
padding: 1rem;
overflow-x: auto;
font-family: 'Space Mono', monospace;
font-size: 0.875rem;
}
.guide-content :global(.copy-button) {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: var(--color-primary-500);
color: white;
border: none;
border-radius: var(--theme-rounded-base);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
cursor: pointer;
transition: background 0.2s;
}
.guide-content :global(.copy-button:hover) {
background: var(--color-primary-600);
}
.guide-content :global(code) {
background-color: var(--color-surface-800);
padding: 0.125rem 0.25rem;
border-radius: var(--theme-rounded-base);
font-family: 'Space Mono', monospace;
font-size: 0.875em;
}
</style>

View File

@ -0,0 +1,296 @@
<script>
import { onMount } from "svelte";
import { Toaster, createToaster, Progress } from "@skeletonlabs/skeleton-svelte";
import { Download, Star, TrendingUp, Award, Users, ArrowUp, ArrowDown } from "lucide-svelte";
// 接收服务端渲染的初始数据
export let initialMirrors = [];
export let initialError = null;
// 状态变量
let mirrors = initialMirrors;
let loading = initialMirrors.length === 0 && !initialError;
let error = initialError;
let timeRange = "month"; // 'week', 'month', 'year'
// Toast通知
const toaster = createToaster();
// 获取镜像数据
async function fetchMirrors() {
try {
loading = true;
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
mirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
// 添加模拟的下载量数据
downloads: Math.floor(Math.random() * 10000),
// 模拟周下载量
weeklyDownloads: Math.floor(Math.random() * 2000),
// 模拟月下载量
monthlyDownloads: Math.floor(Math.random() * 8000),
// 模拟年下载量
yearlyDownloads: Math.floor(Math.random() * 50000),
// 模拟变化趋势(正值表示上升,负值表示下降)
trend: Math.floor(Math.random() * 200) - 100
}));
// 根据选定的时间范围排序
sortMirrorsByTimeRange();
error = null;
} catch (err) {
error = err.message;
toaster.error({
title: "加载失败",
description: `无法获取镜像列表: ${err.message}`,
timeout: 5000
});
} finally {
loading = false;
}
}
// 根据时间范围排序镜像
function sortMirrorsByTimeRange() {
if (timeRange === "week") {
mirrors.sort((a, b) => b.weeklyDownloads - a.weeklyDownloads);
} else if (timeRange === "month") {
mirrors.sort((a, b) => b.monthlyDownloads - a.monthlyDownloads);
} else if (timeRange === "year") {
mirrors.sort((a, b) => b.yearlyDownloads - a.yearlyDownloads);
}
}
// 格式化大数字
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + "M";
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + "K";
}
return num + "";
}
// 获取状态对应的样式
function getStatusBadgeClass(status) {
switch (status) {
case "success":
return "badge-success";
case "failed":
return "badge-error";
case "syncing":
return "badge-warning";
default:
return "badge-surface";
}
}
// 获取趋势图标和颜色
function getTrendInfo(trend) {
if (trend > 0) {
return { icon: ArrowUp, color: "text-success-500", text: `上升 ${trend}%` };
} else if (trend < 0) {
return { icon: ArrowDown, color: "text-error-500", text: `下降 ${Math.abs(trend)}%` };
} else {
return { icon: null, color: "", text: "无变化" };
}
}
// 当时间范围变化时重新排序
$: {
if (mirrors.length > 0) {
sortMirrorsByTimeRange();
}
}
onMount(() => {
// 如果没有初始数据或有错误时,才在客户端请求数据
if (initialMirrors.length === 0 && !initialError) {
fetchMirrors();
}
});
</script>
<div class="container mx-auto">
<div class="card p-4 mb-6">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mb-4">
<div class="btn-group variant-filled-surface">
<button class={`btn ${timeRange === 'week' ? 'variant-filled-primary' : ''}`}
on:click={() => timeRange = 'week'}>
周榜
</button>
<button class={`btn ${timeRange === 'month' ? 'variant-filled-primary' : ''}`}
on:click={() => timeRange = 'month'}>
月榜
</button>
<button class={`btn ${timeRange === 'year' ? 'variant-filled-primary' : ''}`}
on:click={() => timeRange = 'year'}>
年榜
</button>
</div>
<button class="btn variant-filled-primary" on:click={fetchMirrors}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise"
viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path
d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>
<span class="ml-1">刷新数据</span>
</button>
</div>
<!-- 加载状态 -->
{#if loading}
<div class="flex justify-center p-10">
<div class="loader h-10 w-10"></div>
</div>
<!-- 错误信息 -->
{:else if error}
<div class="alert variant-filled-error">
<p>加载失败: {error}</p>
<button class="btn variant-filled" on:click={fetchMirrors}>重试</button>
</div>
<!-- 数据显示 -->
{:else}
<div class="space-y-6">
<!-- 前三名突出显示 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{#each mirrors.slice(0, 3) as mirror, i}
<a href={`/mirror/${mirror.name}`}
class="card p-4 hover:bg-surface-200-700-token transition-all duration-200 hover:-translate-y-1">
<header class="flex items-center gap-2 mb-3">
{#if i === 0}
<div class="text-2xl text-yellow-500">🥇</div>
{:else if i === 1}
<div class="text-2xl text-slate-300">🥈</div>
{:else if i === 2}
<div class="text-2xl text-amber-600">🥉</div>
{/if}
<h3 class="h3 line-clamp-1">{mirror.name}</h3>
</header>
<div class="flex justify-between items-center mb-2">
<span class="badge {getStatusBadgeClass(mirror.status)}">
{mirror.status === 'success' ? '正常' : mirror.status === 'failed' ? '失败' : mirror.status === 'syncing' ? '同步中' : mirror.status}
</span>
{#if mirror.trend !== undefined}
{@const trendInfo = getTrendInfo(mirror.trend)}
<div class="flex items-center gap-1 {trendInfo.color}">
{#if trendInfo.icon}
<svelte:component this={trendInfo.icon} size={14} />
{/if}
<span class="text-xs">{trendInfo.text}</span>
</div>
{/if}
</div>
<div class="mb-4">
<div class="flex justify-between text-xs mb-1">
<span>下载量</span>
<span class="font-medium">
{timeRange === 'week' ? formatNumber(mirror.weeklyDownloads) :
timeRange === 'month' ? formatNumber(mirror.monthlyDownloads) :
formatNumber(mirror.yearlyDownloads)}
</span>
</div>
<Progress
value={i === 0 ? 100 :
i === 1 ? Math.floor(Math.random() * 30) + 60 :
Math.floor(Math.random() * 20) + 40}
max={100}
height="h-2"
meter="bg-primary-500"
track="bg-surface-300-600-token"
/>
</div>
<div class="flex justify-between text-surface-600-300-token text-xs">
<span>大小: {mirror.size || 'N/A'}</span>
<span>更新: {mirror.lastUpdated}</span>
</div>
</a>
{/each}
</div>
<!-- 排行榜列表 -->
<div class="card p-4 variant-glass">
<h3 class="h4 mb-4">更多热门镜像</h3>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th class="w-16">排名</th>
<th>镜像名称</th>
<th>状态</th>
<th class="text-right">下载量</th>
<th class="text-right">趋势</th>
<th>最后更新</th>
</tr>
</thead>
<tbody>
{#each mirrors.slice(3, 20) as mirror, i}
<tr class="hover:bg-surface-200-700-token transition-colors duration-200 cursor-pointer"
on:click={() => window.location.href = `/mirror/${mirror.name}`}>
<td class="font-bold">{i + 4}</td>
<td>{mirror.name}</td>
<td>
<span class="badge {getStatusBadgeClass(mirror.status)}">
{mirror.status === 'success' ? '正常' : mirror.status === 'failed' ? '失败' : mirror.status === 'syncing' ? '同步中' : mirror.status}
</span>
</td>
<td class="text-right font-mono">
{timeRange === 'week' ? formatNumber(mirror.weeklyDownloads) :
timeRange === 'month' ? formatNumber(mirror.monthlyDownloads) :
formatNumber(mirror.yearlyDownloads)}
</td>
<td class="text-right">
{#if mirror.trend !== undefined}
{@const trendInfo = getTrendInfo(mirror.trend)}
<div class="flex items-center justify-end gap-1 {trendInfo.color}">
{#if trendInfo.icon}
<svelte:component this={trendInfo.icon} size={14} />
{/if}
<span class="text-xs">{trendInfo.text}</span>
</div>
{/if}
</td>
<td>{mirror.lastUpdated}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
{/if}
</div>
</div>
<style>
/* 美化卡片动画效果 */
a.card {
cursor: pointer;
box-shadow: var(--shadow-md);
}
/* 表格行动画效果 */
tr {
cursor: pointer;
}
</style>
<Toaster {toaster}></Toaster>

View File

@ -1,210 +0,0 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>

View File

@ -1,30 +1,71 @@
--- ---
// src/layouts/BaseLayout.astro // src/layouts/BasicLayout.astro
import Header from "../components/Header.svelte"; // 引入 Svelte 头部组件 import Header from "../components/Header.svelte";
import Footer from "../components/Footer.svelte"; // 引入 Svelte 页脚组件 import Footer from "../components/Footer.svelte";
import "@/styles/global.css"; // 引入全局样式(包含 Tailwind 和 Skeleton 基础) import "@/styles/global.css";
// 设置 HTML 的 data-theme 属性,让 Skeleton 主题生效 interface Props {
// 注意:你可能需要 client:load 来确保 store 在客户端可用,或者在 <head> 中直接设置 title?: string;
// 一个简单的示例,实际可能更复杂,取决于你的主题切换实现 description?: string;
// const currentTheme = $theme; // 如果在 Astro 中直接读取 store image?: string;
canonicalURL?: string;
}
const {
title = "我的镜像站 | 高速文件下载服务",
description = "提供各类开源软件、系统镜像的高速下载服务",
image = "/images/social-preview.jpg",
canonicalURL = Astro.url.href,
} = Astro.props;
--- ---
<html lang="zh-CN" data-theme="skeleton"> <html lang="zh-CN" data-theme="skeleton">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>{Astro.props.title || "我的镜像站"}</title>
<meta <!-- SEO -->
name="description" <title>{title}</title>
content={Astro.props.description || "文件镜像服务"} <meta name="description" content={description} />
/> <link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url.href} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{image && <meta property="og:image" content={new URL(image, Astro.url)} />}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{image && <meta name="twitter:image" content={new URL(image, Astro.url)} />}
<!-- 预连接常用的域名,提高性能 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- 主题切换脚本,避免闪烁 -->
<script is:inline>
// 立即检查并应用保存的主题,防止闪烁
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'skeleton-dark');
} else {
document.documentElement.classList.remove('dark');
document.documentElement.setAttribute('data-theme', 'skeleton');
}
</script>
</head> </head>
<body class="bg-surface-100-800-token"> <body class="bg-surface-100-800-token min-h-screen flex flex-col">
<Header client:load /> <Header client:load />
<main class="container mx-auto p-4"> <main class="container mx-auto p-4 flex-grow">
<slot /> <slot />
</main> </main>
<Footer client:idle /> <Footer client:idle />

187
src/pages/about.astro Normal file
View File

@ -0,0 +1,187 @@
---
import BasicLayout from "../layouts/BasicLayout.astro";
import "@/styles/global.css";
const pageTitle = "关于我的镜像站 | 高速文件下载服务";
const pageDescription = "了解我们的开源镜像站,使用指南和镜像同步机制说明。";
// 镜像站信息
const siteInfo = {
name: "我的开源镜像站",
description: "为中国开源社区提供高速、稳定、可靠的开源软件镜像服务",
founded: "2023年",
server: {
location: "中国大陆",
bandwidth: "10 Gbps",
storage: "100+ TB",
},
statistics: {
mirrors: "150+",
dailyUsers: "10,000+",
weeklyDownloads: "1,000,000+",
}
};
// 常见问题
const faqs = [
{
question: "镜像是如何同步的?",
answer: "我们使用开源的tunasync系统进行镜像同步根据不同镜像的更新频率设置定时任务确保数据的及时性和准确性。",
},
{
question: "如何报告问题或请求添加新镜像?",
answer: "您可以通过我们的GitHub项目或发送邮件到mirror-admin@example.com联系我们我们会尽快回复并处理您的请求。",
},
{
question: "服务有什么使用限制吗?",
answer: "我们的镜像服务对所有用户免费开放,但请合理使用带宽资源。如需进行大规模下载,请考虑在非高峰时段进行或提前联系我们。",
},
{
question: "镜像数据多久更新一次?",
answer: "不同类型的镜像有不同的更新周期,从每小时到每天不等。您可以在每个镜像的详情页面查看具体的同步状态和上次更新时间。",
}
];
// 联系信息
const contacts = {
email: "mirror-admin@example.com",
github: "https://github.com/yourusername/my-mirror",
twitter: "https://twitter.com/mymirror",
};
---
<BasicLayout title={pageTitle} description={pageDescription}>
<div class="container mx-auto px-4 py-6">
<div class="mb-8">
<h1 class="from-primary-500 to-tertiary-500 bg-gradient-to-r bg-clip-text text-4xl font-bold text-transparent mb-4">
关于我的镜像站
</h1>
<p class="text-lg text-surface-700-200-token">
了解我们的服务、使用方法和最佳实践
</p>
</div>
<!-- 介绍部分 -->
<div class="card variant-glass p-6 mb-8">
<div class="flex flex-col md:flex-row gap-8">
<div class="flex-1">
<h2 class="h2 mb-4">我们的使命</h2>
<p class="mb-4">
{siteInfo.description}。作为开源社区的一份子,我们致力于通过提供高速、可靠的镜像服务,降低开发者获取开源资源的障碍,促进开源软件在国内的发展和应用。
</p>
<p>
成立于{siteInfo.founded},我们持续优化和扩展镜像内容,目前已收录超过{siteInfo.statistics.mirrors}个开源项目的镜像,每周为用户提供超过{siteInfo.statistics.weeklyDownloads}次的下载服务。
</p>
</div>
<div class="md:w-1/3">
<div class="card variant-filled-primary p-4">
<h3 class="h3 mb-3">服务器信息</h3>
<ul class="space-y-2">
<li class="flex justify-between">
<span>位置</span>
<span class="font-bold">{siteInfo.server.location}</span>
</li>
<li class="flex justify-between">
<span>带宽</span>
<span class="font-bold">{siteInfo.server.bandwidth}</span>
</li>
<li class="flex justify-between">
<span>存储容量</span>
<span class="font-bold">{siteInfo.server.storage}</span>
</li>
<li class="flex justify-between">
<span>每日用户</span>
<span class="font-bold">{siteInfo.statistics.dailyUsers}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 使用指南 -->
<div class="card p-6 mb-8">
<h2 class="h2 mb-4">使用指南</h2>
<div class="space-y-6">
<div>
<h3 class="h3 mb-2">基本使用方法</h3>
<p class="mb-4">您可以直接通过网页浏览我们的镜像列表,点击感兴趣的镜像进入详情页面,然后按照说明进行配置使用。</p>
</div>
<div>
<h3 class="h3 mb-2">配置软件源</h3>
<p class="mb-2">很多开源软件工具都支持配置镜像源,以下是一些常见示例:</p>
<div class="mb-4">
<p class="font-bold mb-1">Ubuntu/Debian</p>
<pre class="code language-bash rounded-container-token p-4 overflow-x-auto bg-surface-200-700-token">
sudo sed -i 's/archive.ubuntu.com/mirrors.example.com/g' /etc/apt/sources.list
sudo apt update</pre>
</div>
<div class="mb-4">
<p class="font-bold mb-1">Python pip</p>
<pre class="code language-bash rounded-container-token p-4 overflow-x-auto bg-surface-200-700-token">
pip config set global.index-url https://mirrors.example.com/pypi/simple</pre>
</div>
<div>
<p class="font-bold mb-1">Node.js npm</p>
<pre class="code language-bash rounded-container-token p-4 overflow-x-auto bg-surface-200-700-token">
npm config set registry https://mirrors.example.com/npm/</pre>
</div>
</div>
<div>
<h3 class="h3 mb-2">HTTP协议访问</h3>
<p>所有镜像均支持HTTP和HTTPS协议访问您可以直接在浏览器中输入URL或使用wget、curl等工具下载文件。</p>
</div>
</div>
</div>
<!-- 常见问题 -->
<div class="card p-6 mb-8">
<h2 class="h2 mb-4">常见问题</h2>
<div class="space-y-4">
{faqs.map((faq, index) => (
<div class="card variant-ghost p-4">
<h3 class="font-bold text-lg mb-2">Q{index + 1}: {faq.question}</h3>
<p>{faq.answer}</p>
</div>
))}
</div>
</div>
<!-- 联系我们 -->
<div class="card p-6">
<h2 class="h2 mb-4">联系我们</h2>
<p class="mb-4">如果您有问题、建议或希望参与贡献,欢迎通过以下方式联系我们:</p>
<div class="flex flex-col md:flex-row gap-4">
<a href={`mailto:${contacts.email}`} class="btn variant-filled flex-1 justify-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-envelope" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
{contacts.email}
</a>
<a href={contacts.github} target="_blank" rel="noopener noreferrer" class="btn variant-filled-secondary flex-1 justify-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
GitHub
</a>
<a href={contacts.twitter} target="_blank" rel="noopener noreferrer" class="btn variant-filled-tertiary flex-1 justify-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-twitter" viewBox="0 0 16 16">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"/>
</svg>
Twitter
</a>
</div>
</div>
</div>
</BasicLayout>

70
src/pages/browse.astro Normal file
View File

@ -0,0 +1,70 @@
---
import BasicLayout from "../layouts/BasicLayout.astro";
import BrowseMirrors from "../components/BrowseMirrors.svelte";
import "@/styles/global.css";
const pageTitle = "浏览镜像 | 我的镜像站";
const pageDescription = "浏览和搜索所有可用的开源软件镜像,按类别筛选并查看详情。";
// 在服务端获取镜像数据
let initialMirrors = [];
let initialError = null;
let categories = [];
try {
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
initialMirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
// 分配一个类别(这里使用示例分类,实际数据中可能需要其他方式确定类别)
category: determineCategory(name),
}));
// 提取所有唯一类别
categories = [...new Set(initialMirrors.map(mirror => mirror.category))];
} catch (err) {
initialError = err.message;
console.error("服务端获取数据失败:", err);
}
// 根据名称确定类别的辅助函数(示例实现)
function determineCategory(name) {
if (/ubuntu|debian|fedora|centos|arch|opensuse/i.test(name)) return "Linux发行版";
if (/python|npm|maven|gradle|rust|cargo/i.test(name)) return "开发工具";
if (/apache|nginx|tomcat|docker|kubernetes/i.test(name)) return "服务器软件";
if (/gcc|llvm|clang|make|cmake/i.test(name)) return "编译工具";
if (/mysql|postgresql|mongodb|redis|elasticsearch/i.test(name)) return "数据库";
if (/anaconda|tensorflow|pytorch|scikit/i.test(name)) return "数据科学";
if (/kernel|core|linux/i.test(name)) return "核心组件";
if (/ubuntu|windows|ios|android/i.test(name)) return "操作系统";
return "其他镜像";
}
---
<BasicLayout title={pageTitle} description={pageDescription}>
<div class="container mx-auto px-4 py-6">
<div class="mb-8">
<h1 class="from-primary-500 to-tertiary-500 bg-gradient-to-r bg-clip-text text-4xl font-bold text-transparent mb-4">
浏览镜像
</h1>
<p class="text-lg text-surface-700-200-token">
探索我们的开源软件镜像集合,通过分类浏览或搜索找到您需要的资源
</p>
</div>
<BrowseMirrors
client:load
initialMirrors={initialMirrors}
initialError={initialError}
categories={categories}
/>
</div>
</BasicLayout>

View File

@ -1,16 +1,60 @@
--- ---
import Welcome from "../components/Welcome.astro";
import BasicLayout from "../layouts/BasicLayout.astro"; import BasicLayout from "../layouts/BasicLayout.astro";
import Test from "../components/Test.svelte";
import "@/styles/global.css";
import MirrorList from "../components/MirrorList.svelte"; import MirrorList from "../components/MirrorList.svelte";
import PopularMirrors from "../components/PopularMirrors.svelte";
import "@/styles/global.css";
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build const pageTitle = "我的镜像站 | 高速文件下载服务";
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. const pageDescription =
"提供各类开源软件、系统镜像的高速下载服务,让您快速获取需要的资源。";
// 在服务端获取镜像数据
let initialMirrors = [];
let initialError = null;
try {
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
initialMirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
}));
} catch (err) {
initialError = err.message;
console.error("服务端获取数据失败:", err);
}
--- ---
<BasicLayout> <BasicLayout title={pageTitle} description={pageDescription}>
<!--<Welcome />--> <div class="mx-auto w-full max-w-6xl">
<!--<Test />--> <section class="mb-8">
<MirrorList client:load /> <h1
class="from-primary-500 to-tertiary-500 mb-4 bg-gradient-to-r bg-clip-text text-center text-4xl font-bold text-transparent"
>
欢迎使用我的镜像站
</h1>
<p class="text-surface-700-200-token mb-6 text-center text-lg">
快速、稳定、可靠的文件下载服务
</p>
<div class="mb-6 flex justify-center">
<a href="/mirrors" class="btn variant-filled-primary">查看所有镜像</a>
</div>
</section>
<section class="mb-8">
<h2 class="h2 mb-6">热门镜像</h2>
<PopularMirrors
client:load
initialMirrors={initialMirrors}
initialError={initialError}
/>
</section>
</div>
</BasicLayout> </BasicLayout>

View File

@ -0,0 +1,68 @@
---
import BasicLayout from "../../layouts/BasicLayout.astro";
import MirrorUsageGuide from "../../components/MirrorUsageGuide.svelte";
import "@/styles/global.css";
// 获取路由参数
const { slug } = Astro.params;
// 在服务端获取镜像数据
let mirror = null;
let error = null;
try {
// 获取所有镜像数据
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
// 查找匹配的镜像
if (data[slug]) {
mirror = {
name: slug,
...data[slug],
// 格式化最后更新时间
lastUpdated: new Date(data[slug].last_update_ts * 1000).toLocaleString("zh-CN"),
};
} else {
error = "找不到该镜像";
}
} catch (err) {
error = err.message;
console.error("服务端获取数据失败:", err);
}
// 设置页面标题和描述
const pageTitle = mirror ? `${mirror.name} 镜像使用教程` : "镜像使用教程";
const pageDescription = mirror
? `了解如何使用${mirror.name}镜像源,包括配置指南、常见问题解答等详细教程。`
: "镜像源使用教程,包括配置指南和常见问题解答。";
---
<BasicLayout title={pageTitle} description={pageDescription}>
<div class="mx-auto w-full max-w-6xl">
<div class="breadcrumb mb-6">
<ol class="flex text-sm">
<li class="crumb"><a href="/" class="anchor">首页</a></li>
<li class="crumb-separator">{'/'}</li>
<li class="crumb"><a href="/mirrors" class="anchor">镜像列表</a></li>
<li class="crumb-separator">{'/'}</li>
<li class="crumb">{mirror?.name || "未找到"}</li>
</ol>
</div>
{error ? (
<div class="card p-4 variant-filled-error">
<p class="h3">错误</p>
<p>{error}</p>
<a href="/" class="btn variant-filled mt-4">返回首页</a>
</div>
) : (
<MirrorUsageGuide client:load mirror={mirror} />
)}
</div>
</BasicLayout>

56
src/pages/mirrors.astro Normal file
View File

@ -0,0 +1,56 @@
---
import BasicLayout from "../layouts/BasicLayout.astro";
import MirrorList from "../components/MirrorList.svelte";
import "@/styles/global.css";
const pageTitle = "镜像列表 | 开源软件镜像服务";
const pageDescription = "浏览所有可用的镜像源,包括开源系统、软件仓库等资源。";
// 在服务端获取镜像数据
let initialMirrors = [];
let initialError = null;
try {
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
initialMirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
}));
} catch (err) {
initialError = err.message;
console.error("服务端获取数据失败:", err);
}
---
<BasicLayout title={pageTitle} description={pageDescription}>
<div class="mx-auto w-full max-w-6xl">
<div class="breadcrumb mb-6">
<ol class="flex text-sm">
<li class="crumb"><a href="/" class="anchor">首页</a></li>
<li class="crumb-separator">{'/'}</li>
<li class="crumb">镜像列表</li>
</ol>
</div>
<section class="mb-8">
<h1 class="h1 mb-4">镜像列表</h1>
<p class="text-lg">
这里列出了我们提供的所有镜像源。点击任意镜像名称可以查看详细的使用指南。
</p>
</section>
<MirrorList
client:load
initialMirrors={initialMirrors}
initialError={initialError}
/>
</div>
</BasicLayout>

55
src/pages/new.astro Normal file
View File

@ -0,0 +1,55 @@
---
import BasicLayout from "../layouts/BasicLayout.astro";
import NewMirrors from "../components/NewMirrors.svelte";
import "@/styles/global.css";
const pageTitle = "最新镜像 | 我的镜像站";
const pageDescription = "查看最近更新的开源软件镜像,了解最新的资源变化。";
// 在服务端获取镜像数据
let initialMirrors = [];
let initialError = null;
try {
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
initialMirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
// 原始时间戳
timestampMs: info.last_update_ts * 1000,
}));
// 按更新时间降序排序,最新的排在前面
initialMirrors.sort((a, b) => b.timestampMs - a.timestampMs);
} catch (err) {
initialError = err.message;
console.error("服务端获取数据失败:", err);
}
---
<BasicLayout title={pageTitle} description={pageDescription}>
<div class="container mx-auto px-4 py-6">
<div class="mb-8">
<h1 class="from-primary-500 to-tertiary-500 bg-gradient-to-r bg-clip-text text-4xl font-bold text-transparent mb-4">
最新镜像
</h1>
<p class="text-lg text-surface-700-200-token">
最近更新的开源软件镜像,按更新时间排序
</p>
</div>
<NewMirrors
client:load
initialMirrors={initialMirrors}
initialError={initialError}
/>
</div>
</BasicLayout>

57
src/pages/popular.astro Normal file
View File

@ -0,0 +1,57 @@
---
import BasicLayout from "../layouts/BasicLayout.astro";
import PopularMirrors from "../components/PopularMirrors.svelte";
import "@/styles/global.css";
const pageTitle = "热门镜像 | 我的镜像站";
const pageDescription = "查看最受欢迎的开源软件镜像,基于下载量和用户喜好排序。";
// 在服务端获取镜像数据
let initialMirrors = [];
let initialError = null;
try {
const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
initialMirrors = Object.entries(data).map(([name, info]) => ({
name,
...info,
// 格式化最后更新时间
lastUpdated: new Date(info.last_update_ts * 1000).toLocaleString("zh-CN"),
// 添加模拟的下载量数据(在实际应用中会从真实统计中获取)
downloads: Math.floor(Math.random() * 10000),
// 添加模拟的受欢迎度分数
popularity: Math.floor(Math.random() * 100)
}));
// 按下载量排序
initialMirrors.sort((a, b) => b.downloads - a.downloads);
} catch (err) {
initialError = err.message;
console.error("服务端获取数据失败:", err);
}
---
<BasicLayout title={pageTitle} description={pageDescription}>
<div class="container mx-auto px-4 py-6">
<div class="mb-8">
<h1 class="from-primary-500 to-tertiary-500 bg-gradient-to-r bg-clip-text text-4xl font-bold text-transparent mb-4">
热门镜像
</h1>
<p class="text-lg text-surface-700-200-token">
这些是用户最常使用的开源软件镜像,根据下载量和使用频率排序
</p>
</div>
<PopularMirrors
client:load
initialMirrors={initialMirrors}
initialError={initialError}
/>
</div>
</BasicLayout>