feat: 初步项目结构
This commit is contained in:
parent
9ae820a796
commit
37567f4ef8
337
src/components/BrowseMirrors.svelte
Normal file
337
src/components/BrowseMirrors.svelte
Normal 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>
|
||||||
@ -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>
|
||||||
@ -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)}">
|
||||||
|
|||||||
334
src/components/MirrorUsageGuide.svelte
Normal file
334
src/components/MirrorUsageGuide.svelte
Normal 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>
|
||||||
296
src/components/PopularMirrors.svelte
Normal file
296
src/components/PopularMirrors.svelte
Normal 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>
|
||||||
@ -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>
|
|
||||||
@ -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
187
src/pages/about.astro
Normal 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
70
src/pages/browse.astro
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
68
src/pages/mirror/[slug].astro
Normal file
68
src/pages/mirror/[slug].astro
Normal 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
56
src/pages/mirrors.astro
Normal 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
55
src/pages/new.astro
Normal 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
57
src/pages/popular.astro
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user