feat: 顶栏及其他样式美化

This commit is contained in:
grtsinry43 2025-04-26 10:57:25 +08:00
parent 37567f4ef8
commit ff93cc9444
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
7 changed files with 145 additions and 171 deletions

View File

@ -14,6 +14,7 @@
"@skeletonlabs/skeleton-svelte": "^1.2.1", "@skeletonlabs/skeleton-svelte": "^1.2.1",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"astro": "^5.7.5", "astro": "^5.7.5",
"daisyui": "^5.0.28",
"lucide-svelte": "^0.503.0", "lucide-svelte": "^0.503.0",
"svelte": "^5.28.2", "svelte": "^5.28.2",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",

8
pnpm-lock.yaml generated
View File

@ -23,6 +23,9 @@ importers:
astro: astro:
specifier: ^5.7.5 specifier: ^5.7.5
version: 5.7.5(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.0)(typescript@5.8.3)(yaml@2.7.1) version: 5.7.5(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.40.0)(typescript@5.8.3)(yaml@2.7.1)
daisyui:
specifier: ^5.0.28
version: 5.0.28
lucide-svelte: lucide-svelte:
specifier: ^0.503.0 specifier: ^0.503.0
version: 0.503.0(svelte@5.28.2) version: 0.503.0(svelte@5.28.2)
@ -1095,6 +1098,9 @@ packages:
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
daisyui@5.0.28:
resolution: {integrity: sha512-H082p8Lg3c7Se9wTbjfSOOhfUbp3BnOM2+cdr3OeY5G1Ll7GYLXB9NWLHgitkTsB1pQKwHRYYchqN2YG0VVShg==}
debug@4.4.0: debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -3588,6 +3594,8 @@ snapshots:
csstype@3.1.3: {} csstype@3.1.3: {}
daisyui@5.0.28: {}
debug@4.4.0: debug@4.4.0:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3

View File

@ -1,10 +1,9 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Toaster, createToaster } from "@skeletonlabs/skeleton-svelte"; import { fade, fly, slide } from "svelte/transition";
import { fade, fly } from "svelte/transition";
// 导入Lucide图标 // 导入 Lucide 图标
import { Search, Moon, Sun, Menu, X } from "lucide-svelte"; import { Search, Moon, Sun, Menu, X, Server, Download, Globe, Shield, Info } from "lucide-svelte";
// 状态管理 // 状态管理
let darkMode = false; let darkMode = false;
@ -12,8 +11,14 @@
let searchQuery = ""; let searchQuery = "";
let isScrolled = false; let isScrolled = false;
// Toast通知 // 导航项
const toaster = createToaster(); const navItems = [
{ name: "首页", href: "/", icon: Globe },
{ name: "镜像列表", href: "/mirrors", icon: Server },
{ name: "下载中心", href: "/downloads", icon: Download },
{ name: "状态监控", href: "/status", icon: Shield },
{ name: "关于我们", href: "/about", icon: Info }
];
// 切换主题 // 切换主题
function toggleTheme() { function toggleTheme() {
@ -23,37 +28,13 @@
document.documentElement.classList.toggle("dark", darkMode); document.documentElement.classList.toggle("dark", darkMode);
document.documentElement.setAttribute( document.documentElement.setAttribute(
"data-theme", "data-theme",
darkMode ? "skeleton-dark" : "skeleton" darkMode ? "dark" : "corporate"
); );
// 保存用户偏好 // 保存用户偏好
if (typeof localStorage !== "undefined") { if (typeof localStorage !== "undefined") {
localStorage.setItem("theme", darkMode ? "dark" : "light"); localStorage.setItem("theme", darkMode ? "dark" : "light");
} }
// 显示主题切换通知
toaster.info({
type: "info",
title: "主题切换",
description: `已切换到${darkMode ? "暗色" : "亮色"}主题`,
background: "variant-filled-primary",
timeout: 2000
});
}
// 处理搜索
function handleSearch(e) {
if (e.key === "Enter" || e.type === "click") {
if (searchQuery.trim()) {
toaster.trigger({
message: `正在搜索: ${searchQuery}`,
background: "variant-filled-secondary",
timeout: 2000
});
// 这里可以添加实际的搜索逻辑
searchQuery = "";
}
}
} }
// 监听滚动事件 // 监听滚动事件
@ -67,11 +48,9 @@
if (typeof localStorage !== "undefined" && localStorage.getItem("theme") === "dark") { if (typeof localStorage !== "undefined" && localStorage.getItem("theme") === "dark") {
darkMode = true; darkMode = true;
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
document.documentElement.setAttribute("data-theme", "skeleton-dark");
} else if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) { } else if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
darkMode = true; darkMode = true;
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
document.documentElement.setAttribute("data-theme", "skeleton-dark");
} }
// 滚动监听 // 滚动监听
@ -85,114 +64,85 @@
</script> </script>
<header <header
class="sticky top-0 z-50 w-full transition-all duration-300 {isScrolled ? 'shadow-lg' : 'shadow-md'} class="sticky top-0 z-50 w-full transition-all duration-300 {isScrolled ? 'shadow-md' : ''}
{isScrolled ? 'bg-surface-100-800-token/95' : 'bg-surface-100-800-token/80'} {isScrolled ? 'bg-base-100/95' : 'bg-base-100'}
backdrop-blur-2xl border-b border-surface-300-600-token"> backdrop-blur-sm border-b border-neutral-content/10">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4 max-w-7xl">
<div class="flex items-center justify-between h-16"> <div class="flex items-center gap-8 h-16 lg:h-20">
<!-- 网站Logo和标题 --> <!-- 网站 Logo 和标题 -->
<div class="flex items-center"> <div class="flex items-center">
<a href="/" class="flex items-center space-x-2 group"> <a href="/" class="flex items-center space-x-3 group">
<div class="w-8 h-8 rounded-full bg-primary-500 flex items-center justify-center text-white font-bold <div class="w-10 h-10 bg-neutral text-neutral-content flex items-center justify-center font-bold text-xl
transition-all duration-300 group-hover:scale-110">M transition-all duration-300 group-hover:shadow-sm">
<Server size={20} strokeWidth={1.5} />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold text-base-content tracking-wide uppercase">Mirror Center</span>
<span class="text-xs text-base-content/70 -mt-1 tracking-wider"> 官方开源软件镜像站 </span>
</div> </div>
<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 lg:flex items-center space-x-1">
<a href="/" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">首页</a> {#each navItems as item}
<a href="/browse" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">浏览</a> <a
<a href="/popular" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">热门</a> href={item.href}
<a href="/new" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">最新</a> class="flex items-center px-4 py-2 rounded text-base-content/80 hover:text-primary hover:bg-base-200/30 transition-all duration-200 font-medium"
<a href="/about" class="btn btn-sm variant-ghost-surface hover:variant-soft-primary">关于</a> >
<svelte:component this={item.icon} size={16} class="mr-2" strokeWidth={1.5} />
{item.name}
</a>
{/each}
</nav> </nav>
<!-- 搜索和主题切换 --> <div class="flex items-center gap-2 ml-auto">
<div class="flex items-center space-x-2">
<!-- 搜索框 -->
<div class="relative hidden md:block">
<input
type="search"
bind:value={searchQuery}
on:keydown={handleSearch}
placeholder="搜索镜像..."
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
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}
>
<Search size={18} />
</button>
</div>
<!-- 主题切换 --> <!-- 主题切换 -->
<button <button
class="btn btn-sm variant-ghost-surface aspect-square hover:variant-soft-primary" class="btn btn-sm btn-ghost hover:bg-base-200/70"
on:click={toggleTheme} on:click={toggleTheme}
aria-label="切换主题" aria-label={darkMode ? "切换到亮色模式" : "切换到暗色模式"}
title={darkMode ? "切换至亮色模式" : "切换至暗色模式"}
> >
{#if darkMode} {#if darkMode}
<Sun size={18} class="transition-transform hover:rotate-45 duration-300" /> <Sun size={18} strokeWidth={1.5} />
{:else} {:else}
<Moon size={18} class="transition-transform hover:rotate-12 duration-300" /> <Moon size={18} strokeWidth={1.5} />
{/if} {/if}
</button> </button>
<!-- 移动端菜单按钮 --> <!-- 移动端菜单按钮 -->
<button <button
class="btn btn-sm variant-ghost-surface md:hidden aspect-square hover:variant-soft-primary" class="btn btn-sm btn-ghost lg:hidden hover:bg-base-200/70"
on:click={() => mobileMenuOpen = !mobileMenuOpen} on:click={()=> (mobileMenuOpen = !mobileMenuOpen)}
aria-label="菜单" aria-label="菜单"
aria-expanded={mobileMenuOpen} aria-expanded={mobileMenuOpen}
> >
{#if mobileMenuOpen} {#if mobileMenuOpen}
<X size={18} /> <X size={18} strokeWidth={1.5} />
{:else} {:else}
<Menu size={18} /> <Menu size={18} strokeWidth={1.5} />
{/if} {/if}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<Toaster {toaster}></Toaster>
<!-- 移动端菜单 --> <!-- 移动端菜单 -->
{#if mobileMenuOpen} {#if mobileMenuOpen}
<div class="md:hidden bg-surface-100-800-token border-t border-surface-300-600-token" <div
transition:fly={{ y: -10, duration: 200 }}> class="lg:hidden bg-base-100 border-t border-neutral-content/10"
<div class="container mx-auto px-4 py-3 space-y-2"> transition:slide={{duration: 200}}
<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 hover:variant-soft-primary justify-start">浏览</a>
<a href="/popular"
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">
<input
type="search"
bind:value={searchQuery}
on:keydown={handleSearch}
placeholder="搜索镜像..."
class="input input-sm pl-9 pr-4 rounded-full w-full focus:ring-primary-500"
/>
<button
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}
> >
<Search size={18} /> <!-- 移动端导航链接 -->
</button> {#each navItems as item}
</div> <a
</div> href={item.href}
class="flex items-center px-4 py-3 rounded text-base-content/80 hover:text-primary hover:bg-base-200/30 transition-all duration-200 font-medium"
>
<svelte:component this={item.icon} size={18} class="mr-3" strokeWidth={1.5} />
{item.name}
</a>
{/each}
</div> </div>
{/if} {/if}
</header> </header>
@ -200,16 +150,25 @@
<style> <style>
/* 活动链接样式 */ /* 活动链接样式 */
:global(a.active) { :global(a.active) {
/*@apply bg-primary-500/20 text-primary-500;*/ /*@apply text-primary bg-primary/5 font-semibold;*/
}
/* 链接悬停效果 */
a.btn:hover {
/*@apply bg-primary-500/10;*/
} }
/* 平滑过渡 */ /* 平滑过渡 */
a, button { a, button {
/*@apply transition-all duration-200;*/ @apply transition-all duration-200;
}
/* 自定义滚动条 */
:global(::-webkit-scrollbar) {
width: 8px;
height: 8px;
}
:global(::-webkit-scrollbar-track) {
/*@apply bg-base-200/30;*/
}
:global(::-webkit-scrollbar-thumb) {
/*@apply bg-neutral-content/20 rounded-full hover:bg-neutral-content/30;*/
} }
</style> </style>

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 { RefreshCw, Search } from "lucide-svelte";
// 接收服务端渲染的初始数据 // 接收服务端渲染的初始数据
export let initialMirrors = []; export let initialMirrors = [];
@ -13,7 +14,7 @@
let searchTerm = ""; let searchTerm = "";
let sortBy = "name"; // 默认按名称排序 let sortBy = "name"; // 默认按名称排序
// Toast通知 // Toast 通知
const toaster = createToaster(); const toaster = createToaster();
// 获取镜像数据 // 获取镜像数据
@ -25,7 +26,7 @@
console.log(response); console.log(response);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`); throw new Error(`HTTP 错误: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
@ -41,7 +42,7 @@
console.error(err); console.error(err);
toaster.error({ toaster.error({
title: "加载失败", title: "加载失败",
description: `无法获取镜像列表: ${err.message}`, description: ` 无法获取镜像列表: ${err.message}`,
timeout: 5000 timeout: 5000
}); });
} finally { } finally {
@ -92,32 +93,43 @@
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<div class="card p-4 mb-4"> <div class="card p-4 mb-4">
<h2 class="h2 mb-4">镜像列表</h2> <h2 class="h2 mb-4"> 镜像列表 </h2>
<!-- 搜索和排序控制 --> <div class="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
<div class="flex flex-col sm:flex-row items-center gap-4 mb-4"> <div class="relative w-full sm:w-64">
<div class="input-group input-group-divider grid-cols-[auto_1fr] w-full sm:w-auto">
<input <input
type="search" type="text"
bind:value={searchTerm} bind:value={searchTerm}
placeholder="搜索镜像..." placeholder="搜索镜像..."
class="input" class="input input-bordered w-full pl-10 h-10"
/> />
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
<Search size={16} strokeWidth={1.5} />
</div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex gap-2">
<span>排序:</span> <select bind:value={sortBy} class="select select-bordered h-10 text-sm">
<select bind:value={sortBy} class="select"> <option value="name"> 名称</option>
<option value="name">名称</option> <option value="status"> 状态</option>
<option value="status">状态</option> <option value="date"> 更新时间</option>
<option value="date">更新时间</option> <option value="size"> 大小</option>
</select> </select>
</div>
<button class="btn variant-filled-primary ml-auto" on:click={fetchMirrors}> <button
刷新列表 class="btn btn-ghost h-10 ml-auto"
on:click={fetchMirrors}
disabled={loading}
>
{#if loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<RefreshCw size={16} class="mr-1" />
刷新
{/if}
</button> </button>
</div> </div>
</div>
<!-- 加载状态 --> <!-- 加载状态 -->
{#if loading} {#if loading}
@ -127,8 +139,8 @@
<!-- 错误信息 --> <!-- 错误信息 -->
{:else if error} {:else if error}
<div class="alert variant-filled-error"> <div class="alert variant-filled-error">
<p>加载失败: {error}</p> <p> 加载失败: {error}</p>
<button class="btn variant-filled" on:click={fetchMirrors}>重试</button> <button class="btn variant-filled" on:click={fetchMirrors}> 重试</button>
</div> </div>
<!-- 数据列表 --> <!-- 数据列表 -->
{:else} {:else}
@ -136,10 +148,10 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>镜像名称</th> <th> 镜像名称</th>
<th>状态</th> <th> 状态</th>
<th>大小</th> <th> 大小</th>
<th>最后更新时间</th> <th> 最后更新时间</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -147,10 +159,10 @@
<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)}> 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)} badge-soft">
{mirror.status === 'success' ? '正常' : mirror.status === 'failed' ? '失败' : mirror.status === 'syncing' ? '同步中' : mirror.status} {mirror.status === 'success' ? '正常' : mirror.status === 'failed' ? '失败' : mirror.status === 'syncing' ? '同步中' : mirror.status}
</span> </span>
</td> </td>
@ -159,13 +171,13 @@
</tr> </tr>
{:else} {:else}
<tr> <tr>
<td colspan="4" class="text-center py-4">未找到符合条件的镜像</td> <td colspan="4" class="text-center py-4"> 未找到符合条件的镜像</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
<p class="text-sm mt-2 text-surface-500-400-token">{filteredMirrors.length} 个镜像</p> <p class="text-sm mt-2 text-surface-500-400-token"> {filteredMirrors.length} 个镜像 </p>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@ const {
} = Astro.props; } = Astro.props;
--- ---
<html lang="zh-CN" data-theme="skeleton"> <html lang="zh-CN" data-theme="corporate">
<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" />
@ -56,10 +56,10 @@ const {
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'skeleton-dark'); document.documentElement.setAttribute('data-theme', 'dark');
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
document.documentElement.setAttribute('data-theme', 'skeleton'); document.documentElement.setAttribute('data-theme', 'corporate');
} }
</script> </script>
</head> </head>

View File

@ -16,7 +16,7 @@ try {
const response = await fetch("http://localhost:8082/static/tunasync.json"); const response = await fetch("http://localhost:8082/static/tunasync.json");
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`); throw new Error(`HTTP 错误: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
@ -33,28 +33,24 @@ try {
--- ---
<BasicLayout title={pageTitle} description={pageDescription}> <BasicLayout title={pageTitle} description={pageDescription}>
<div class="mx-auto w-full max-w-6xl"> <div class="mx-auto w-full max-w-7xl">
<section class="mb-8"> {/* Slogan Section based on the image */}
<h1 <section class="text-foreground mb-8 rounded-lg px-6 py-10 text-left">
class="from-primary-500 to-tertiary-500 mb-4 bg-gradient-to-r bg-clip-text text-center text-4xl font-bold text-transparent" {/* Added basic styling similar to image */}
> <h1 class="mb-3 text-4xl font-bold lg:text-5xl">
欢迎使用我的镜像站 Open Source Software Mirror
</h1> </h1>
<p class="text-surface-700-200-token mb-6 text-center text-lg"> <p class="text-lg lg:text-xl">
快速、稳定、可靠的文件下载服务 Welcome to Central South University Mirror Station
</p>
<p class="text-foreground/70 text-sm">
Let's make the world a better place together!
</p> </p>
<div class="mb-6 flex justify-center">
<a href="/mirrors" class="btn variant-filled-primary">查看所有镜像</a>
</div>
</section> </section>
{/* Existing Popular Mirrors Section */}
<section class="mb-8"> <section class="mb-8">
<h2 class="h2 mb-6">热门镜像</h2> <MirrorList initialMirrors={initialMirrors} initialError={initialError} />
<PopularMirrors
client:load
initialMirrors={initialMirrors}
initialError={initialError}
/>
</section> </section>
</div> </div>
</BasicLayout> </BasicLayout>

View File

@ -1,10 +1,8 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import '@skeletonlabs/skeleton'; @plugin "daisyui" {
@import '@skeletonlabs/skeleton/optional/presets'; themes: corporate --default, dark --prefersdark;
@import '@skeletonlabs/skeleton/themes/cerberus'; }
@source '@skeletonlabs/skeleton-svelte/dist';
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));