Cut iconify

This commit is contained in:
2026-05-21 20:59:02 +04:00
parent 73dfd4e750
commit 95342ae758
27 changed files with 733 additions and 686 deletions

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { siteIconFallback, siteIconSvgs } from "@/generated/site-icon-svgs";
interface Props {
icon: string;
class?: string;
}
let { icon, class: className = "" }: Props = $props();
const svg = $derived(siteIconSvgs[icon] ?? siteIconFallback);
</script>
<span class={`inline-flex items-center justify-center shrink-0 [&>svg]:w-[1em] [&>svg]:h-[1em] ${className}`}>
{@html svg}
</span>

View File

@@ -1,6 +1,5 @@
---
// 可靠的图标组件
// 提供加载状态管理和错误处理
import { Icon } from "astro-icon/components";
export interface Props {
icon: string;
@@ -8,8 +7,6 @@ export interface Props {
style?: string;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
color?: string;
fallback?: string; // 备用图标或文本
loading?: "lazy" | "eager";
}
const {
@@ -18,11 +15,8 @@ const {
style = "",
size = "md",
color,
fallback = "●",
loading = "lazy",
} = Astro.props;
// 尺寸映射
const sizeClasses = {
xs: "text-xs",
sm: "text-sm",
@@ -35,147 +29,7 @@ const sizeClasses = {
const sizeClass = sizeClasses[size] || sizeClasses.md;
const colorStyle = color ? `color: ${color};` : "";
const combinedStyle = `${colorStyle}${style}`;
const combinedClass = `${sizeClass} ${className}`.trim();
// 生成唯一ID
const iconId = `icon-${Math.random().toString(36).substr(2, 9)}`;
const combinedClass = `inline-flex items-center justify-center shrink-0 ${sizeClass} ${className}`.trim();
---
<span
class={`inline-flex items-center justify-center ${combinedClass}`}
style={combinedStyle}
data-icon-container={iconId}
>
<!-- 加载状态指示器 -->
<span
class="icon-loading animate-pulse opacity-50"
data-loading-indicator
>
{fallback}
</span>
<!-- 实际图标 -->
<iconify-icon
icon={icon}
class="icon-content opacity-0 transition-opacity duration-200"
data-icon-element
loading={loading}
></iconify-icon>
</span>
<script define:vars={{ iconId, icon }}>
// 图标加载和显示逻辑
(function() {
const container = document.querySelector(`[data-icon-container="${iconId}"]`);
if (!container) return;
const loadingIndicator = container.querySelector('[data-loading-indicator]');
const iconElement = container.querySelector('[data-icon-element]');
if (!loadingIndicator || !iconElement) return;
// 检查图标是否已经加载
function checkIconLoaded() {
// 检查iconify-icon元素是否已经渲染
const hasContent = iconElement.shadowRoot &&
iconElement.shadowRoot.children.length > 0;
if (hasContent) {
showIcon();
return true;
}
return false;
}
// 显示图标,隐藏加载指示器
function showIcon() {
loadingIndicator.style.display = 'none';
iconElement.classList.remove('opacity-0');
iconElement.classList.add('opacity-100');
}
// 显示加载指示器,隐藏图标
function showLoading() {
loadingIndicator.style.display = 'inline-flex';
iconElement.classList.remove('opacity-100');
iconElement.classList.add('opacity-0');
}
// 初始状态
showLoading();
// 监听图标加载事件
iconElement.addEventListener('load', () => {
showIcon();
});
// 监听图标加载错误
iconElement.addEventListener('error', () => {
// 保持显示fallback
console.warn(`Failed to load icon: ${icon}`);
});
// 使用MutationObserver监听shadow DOM变化
if (window.MutationObserver) {
const observer = new MutationObserver(() => {
if (checkIconLoaded()) {
observer.disconnect();
}
});
// 监听iconify-icon元素的变化
observer.observe(iconElement, {
childList: true,
subtree: true,
attributes: true
});
// 设置超时,避免无限等待
setTimeout(() => {
observer.disconnect();
if (!checkIconLoaded()) {
console.warn(`Icon load timeout: ${icon}`);
}
}, 5000);
}
// 立即检查一次(可能已经加载完成)
setTimeout(() => {
checkIconLoaded();
}, 100);
})();
</script>
<style>
.icon-loading {
min-width: 1em;
min-height: 1em;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon-content {
display: inline-flex;
align-items: center;
justify-content: center;
}
[data-icon-container] {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1em;
min-height: 1em;
}
[data-icon-container] .icon-loading,
[data-icon-container] .icon-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<Icon name={icon} class={combinedClass} style={combinedStyle} />

View File

@@ -1,248 +0,0 @@
---
import { url } from "@utils/url";
export interface Props {
preloadIcons?: string[];
}
const { preloadIcons = [] } = Astro.props;
const iconifyScriptUrl = url("/assets/iconify/iconify-icon.min.js");
---
<div
id="iconify-loader-config"
data-script-url={iconifyScriptUrl}
data-preload-icons={JSON.stringify(preloadIcons)}
hidden
aria-hidden="true"
></div>
<script>
import {
registerSvelteIconifyCollections,
registerWebComponentIconifyCollections,
} from "@/utils/iconify-offline";
const LOAD_TIMEOUT_MS = 3000;
const RETRY_COUNT = 1;
(function () {
"use strict";
if (window.__iconifyLoaderInitialized) {
return;
}
window.__iconifyLoaderInitialized = true;
registerSvelteIconifyCollections();
const configEl = document.getElementById("iconify-loader-config");
const iconifyScriptUrl = configEl?.dataset.scriptUrl ?? "/assets/iconify/iconify-icon.min.js";
const preloadIcons = JSON.parse(configEl?.dataset.preloadIcons ?? "[]");
class IconifyLoader {
constructor() {
this.isLoaded = false;
this.isLoading = false;
this.loadPromise = null;
this.observers = new Set();
this.preloadQueue = new Set();
}
async load(options = {}) {
const {
timeout: loadTimeout = LOAD_TIMEOUT_MS,
retryCount: maxRetries = RETRY_COUNT,
} = options;
if (this.isLoaded) {
return Promise.resolve();
}
if (this.isLoading && this.loadPromise) {
return this.loadPromise;
}
this.isLoading = true;
this.loadPromise = this.loadWithRetry(loadTimeout, maxRetries);
try {
await this.loadPromise;
this.isLoaded = true;
this.notifyObservers();
await this.processPreloadQueue();
} catch (error) {
console.error("Failed to load Iconify:", error);
throw error;
} finally {
this.isLoading = false;
}
}
async loadWithRetry(timeoutMs, maxRetries) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.loadScript(timeoutMs);
return;
} catch (error) {
console.warn(`Iconify load attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
throw new Error(
`Failed to load Iconify after ${maxRetries} attempts`,
);
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
}
loadScript(timeoutMs) {
return new Promise((resolve, reject) => {
if (this.isIconifyReady()) {
registerWebComponentIconifyCollections();
resolve();
return;
}
const existingScript = document.querySelector(
'script[src*="iconify-icon"]',
);
if (existingScript) {
this.waitForIconifyReady()
.then(() => {
registerWebComponentIconifyCollections();
resolve();
})
.catch(reject);
return;
}
const script = document.createElement("script");
script.src = iconifyScriptUrl;
script.async = true;
const timeoutId = setTimeout(() => {
script.remove();
reject(new Error("Script load timeout"));
}, timeoutMs);
script.onload = () => {
clearTimeout(timeoutId);
this.waitForIconifyReady()
.then(() => {
registerWebComponentIconifyCollections();
resolve();
})
.catch(reject);
};
script.onerror = () => {
clearTimeout(timeoutId);
script.remove();
reject(new Error("Script load error"));
};
document.head.appendChild(script);
});
}
waitForIconifyReady(maxWait = 5000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkReady = () => {
if (this.isIconifyReady()) {
resolve();
return;
}
if (Date.now() - startTime > maxWait) {
reject(new Error("Iconify initialization timeout"));
return;
}
setTimeout(checkReady, 50);
};
checkReady();
});
}
isIconifyReady() {
return (
typeof window !== "undefined" &&
"customElements" in window &&
customElements.get("iconify-icon") !== undefined
);
}
onLoad(callback) {
if (this.isLoaded) {
callback();
} else {
this.observers.add(callback);
}
}
notifyObservers() {
this.observers.forEach((callback) => {
try {
callback();
} catch (error) {
console.error("Error in icon load observer:", error);
}
});
this.observers.clear();
}
addToPreloadQueue(icons) {
if (Array.isArray(icons)) {
icons.forEach((icon) => this.preloadQueue.add(icon));
} else {
this.preloadQueue.add(icons);
}
if (this.isLoaded) {
this.processPreloadQueue();
}
}
async processPreloadQueue() {
this.preloadQueue.clear();
}
}
window.__iconifyLoader = new IconifyLoader();
window.__iconifyLoader.load().catch((error) => {
console.error("Failed to initialize Iconify:", error);
});
if (preloadIcons.length > 0) {
window.__iconifyLoader.addToPreloadQueue(preloadIcons);
}
window.loadIconify = () => window.__iconifyLoader.load();
window.preloadIcons = (icons) =>
window.__iconifyLoader.addToPreloadQueue(icons);
window.onIconifyReady = (callback) =>
window.__iconifyLoader.onLoad(callback);
document.addEventListener("visibilitychange", () => {
if (!document.hidden && !window.__iconifyLoader.isLoaded) {
window.__iconifyLoader.load().catch(console.error);
}
});
})();
</script>
<noscript>
<style>
iconify-icon {
display: none;
}
.icon-fallback {
display: inline-block;
}
</style>
</noscript>

View File

@@ -218,18 +218,3 @@ const getExperienceText = (experience: Props["skill"]["experience"]) => {
</div>
</div>
</div>
<script>
// 监听图标加载完成事件
document.addEventListener('DOMContentLoaded', () => {
const skillCard = document.currentScript?.parentElement;
if (skillCard) {
skillCard.classList.add('skill-card');
// 监听图标准备就绪事件
skillCard.addEventListener('iconify-ready', () => {
// 图标加载完成,可以执行额外的初始化逻辑
skillCard.classList.add('icons-loaded');
});
}
});
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { slide } from "svelte/transition";
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import type { MusicPlayerTrack } from "@/types/config";
import { musicPlayerConfig } from "@/config";
@@ -557,10 +557,10 @@ onDestroy(() => {
{#if showError}
<div class="music-player-error fixed bottom-20 right-4 z-60 max-w-sm onload-animation-up">
<div class="bg-red-500 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 animate-slide-up">
<Icon icon="material-symbols:error" class="text-xl shrink-0" />
<SiteIcon icon="material-symbols:error" class="text-xl shrink-0" />
<span class="text-sm flex-1">{errorMessage}</span>
<button onclick={hideError} class="text-white/80 hover:text-white transition-colors">
<Icon icon="material-symbols:close" class="text-lg" />
<SiteIcon icon="material-symbols:close" class="text-lg" />
</button>
</div>
</div>
@@ -575,7 +575,7 @@ onDestroy(() => {
<div class="playlist-header flex items-center justify-between p-4 border-b border-(--line-divider)">
<h3 class="text-lg font-semibold text-90">{i18n(Key.playlist)}</h3>
<button class="btn-plain w-8 h-8 rounded-lg" onclick={togglePlaylist}>
<Icon icon="material-symbols:close" class="text-lg" />
<SiteIcon icon="material-symbols:close" class="text-lg" />
</button>
</div>
<div class="playlist-content overflow-y-auto max-h-80">
@@ -595,9 +595,9 @@ onDestroy(() => {
aria-label="播放 {song.title} - {song.artist}">
<div class="w-6 h-6 flex items-center justify-center">
{#if index === currentIndex && isPlaying}
<Icon icon="material-symbols:graphic-eq" class="text-(--primary) animate-pulse" />
<SiteIcon icon="material-symbols:graphic-eq" class="text-(--primary) animate-pulse" />
{:else if index === currentIndex}
<Icon icon="material-symbols:pause" class="text-(--primary)" />
<SiteIcon icon="material-symbols:pause" class="text-(--primary)" />
{:else}
<span class="text-sm text-(--content-meta)">{index + 1}</span>
{/if}
@@ -636,7 +636,7 @@ onDestroy(() => {
tabindex="0"
aria-label={i18n(Key.musicExpand)}>
{#if isLoading}
<Icon icon="eos-icons:loading" class="text-white text-lg" />
<SiteIcon icon="material-symbols:progress-activity" class="text-white text-lg" />
{:else if isPlaying}
<div class="flex space-x-0.5">
<div class="w-0.5 h-3 bg-white rounded-full animate-pulse"></div>
@@ -644,7 +644,7 @@ onDestroy(() => {
<div class="w-0.5 h-2 bg-white rounded-full animate-pulse" style="animation-delay: 300ms;"></div>
</div>
{:else}
<Icon icon="material-symbols:music-note" class="text-white text-lg" />
<SiteIcon icon="material-symbols:music-note" class="text-white text-lg" />
{/if}
</div>
<!-- 展开状态的完整播放器(封面圆形) -->
@@ -671,13 +671,13 @@ onDestroy(() => {
<button class="btn-plain w-8 h-8 rounded-lg flex items-center justify-center"
onclick={toggleMode}
title={mode === "meting" ? i18n(Key.musicSwitchToLocal) : i18n(Key.musicSwitchToMeting)}>
<Icon icon={mode === "meting" ? "material-symbols:cloud" : "material-symbols:folder"} class="text-lg" />
<SiteIcon icon={mode === "meting" ? "material-symbols:cloud" : "material-symbols:folder"} class="text-lg" />
</button>
<button class="btn-plain w-8 h-8 rounded-lg flex items-center justify-center"
class:text-(--primary)={showPlaylist}
onclick={togglePlaylist}
title={i18n(Key.playlist)}>
<Icon icon="material-symbols:queue-music" class="text-lg" />
<SiteIcon icon="material-symbols:queue-music" class="text-lg" />
</button>
</div>
</div>
@@ -717,27 +717,27 @@ onDestroy(() => {
class:btn-plain={!isShuffled}
onclick={toggleShuffle}
disabled={playlist.length <= 1}>
<Icon icon="material-symbols:shuffle" class="text-lg" />
<SiteIcon icon="material-symbols:shuffle" class="text-lg" />
</button>
<button class="btn-plain w-10 h-10 rounded-lg" onclick={previousSong}
disabled={playlist.length <= 1}>
<Icon icon="material-symbols:skip-previous" class="text-xl" />
<SiteIcon icon="material-symbols:skip-previous" class="text-xl" />
</button>
<button class="btn-regular w-12 h-12 rounded-full"
class:opacity-50={isLoading}
disabled={isLoading}
onclick={togglePlay}>
{#if isLoading}
<Icon icon="eos-icons:loading" class="text-xl" />
<SiteIcon icon="material-symbols:progress-activity" class="text-xl" />
{:else if isPlaying}
<Icon icon="material-symbols:pause" class="text-xl" />
<SiteIcon icon="material-symbols:pause" class="text-xl" />
{:else}
<Icon icon="material-symbols:play-arrow" class="text-xl" />
<SiteIcon icon="material-symbols:play-arrow" class="text-xl" />
{/if}
</button>
<button class="btn-plain w-10 h-10 rounded-lg" onclick={nextSong}
disabled={playlist.length <= 1}>
<Icon icon="material-symbols:skip-next" class="text-xl" />
<SiteIcon icon="material-symbols:skip-next" class="text-xl" />
</button>
<!-- 循环按钮高亮 -->
<button class="w-10 h-10 rounded-lg"
@@ -745,22 +745,22 @@ onDestroy(() => {
class:btn-plain={isRepeating === 0}
onclick={toggleRepeat}>
{#if isRepeating === 1}
<Icon icon="material-symbols:repeat-one" class="text-lg" />
<SiteIcon icon="material-symbols:repeat-one" class="text-lg" />
{:else if isRepeating === 2}
<Icon icon="material-symbols:repeat" class="text-lg" />
<SiteIcon icon="material-symbols:repeat" class="text-lg" />
{:else}
<Icon icon="material-symbols:repeat" class="text-lg opacity-50" />
<SiteIcon icon="material-symbols:repeat" class="text-lg opacity-50" />
{/if}
</button>
</div>
<div class="bottom-controls flex items-center gap-2">
<button class="btn-plain w-8 h-8 rounded-lg" onclick={toggleMute}>
{#if isMuted || volume === 0}
<Icon icon="material-symbols:volume-off" class="text-lg" />
<SiteIcon icon="material-symbols:volume-off" class="text-lg" />
{:else if volume < 0.5}
<Icon icon="material-symbols:volume-down" class="text-lg" />
<SiteIcon icon="material-symbols:volume-down" class="text-lg" />
{:else}
<Icon icon="material-symbols:volume-up" class="text-lg" />
<SiteIcon icon="material-symbols:volume-up" class="text-lg" />
{/if}
</button>
<div class="flex-1 h-2 bg-(--btn-regular-bg) rounded-full cursor-pointer"
@@ -787,7 +787,7 @@ onDestroy(() => {
<button class="btn-plain w-8 h-8 rounded-lg flex items-center justify-center"
onclick={toggleCollapse}
title={i18n(Key.musicCollapse)}>
<Icon icon="material-symbols:expand-more" class="text-lg" />
<SiteIcon icon="material-symbols:expand-more" class="text-lg" />
</button>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import { BREAKPOINT_LG } from "@constants/breakpoints";
@@ -58,7 +58,7 @@ $effect(() => {
onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { togglePanel(); } }}
onmouseenter={openPanel}
>
<Icon icon="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:palette-outline" class="text-[1.25rem]"/>
</button>
<div id="display-setting-wrapper" class="fixed top-14.5 pt-5 right-4 w-[calc(100vw-2rem)] max-w-80 md:absolute md:top-11 md:right-0 md:w-80 md:pt-5 transition-all z-50" class:float-panel-closed={!isOpen}>
<div id="display-setting" class="card-base float-panel px-4 py-4 w-full">
@@ -71,7 +71,7 @@ $effect(() => {
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90"
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} onclick={resetHue}>
<div class="text-(--btn-content)">
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
<SiteIcon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"/>
</div>
</button>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import { BREAKPOINT_LG } from "@constants/breakpoints";
@@ -66,13 +66,13 @@ onMount(() => {
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={closePanel}>
<button aria-label="Light/Dark/System Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch" onmouseenter={openPanel} onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { toggleScheme(); } }}>
<div class="absolute" class:opacity-0={mode !== LIGHT_MODE}>
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== DARK_MODE}>
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== SYSTEM_MODE}>
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"/>
</div>
</button>
<div id="light-dark-panel" class="absolute transition top-11 -right-2 pt-5" class:float-panel-closed={!isOpen}>
@@ -82,7 +82,7 @@ onMount(() => {
isLast={false}
onclick={() => switchScheme(LIGHT_MODE)}
>
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.lightMode)}
</DropdownItem>
<DropdownItem
@@ -90,7 +90,7 @@ onMount(() => {
isLast={false}
onclick={() => switchScheme(DARK_MODE)}
>
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.darkMode)}
</DropdownItem>
<DropdownItem
@@ -98,7 +98,7 @@ onMount(() => {
isLast={true}
onclick={() => switchScheme(SYSTEM_MODE)}
>
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.systemMode)}
</DropdownItem>
</DropdownPanel>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import type { NavbarLink } from "@/types/config";
@@ -39,7 +39,7 @@ onMount(() => {
id="nav-menu-switch"
onclick={togglePanel}
>
<Icon icon="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:menu-rounded" class="text-[1.25rem]"/>
</button>
<div id="nav-menu-panel"
class="float-panel fixed transition-all right-4 px-2 py-2 max-h-[80vh] overflow-y-auto"
@@ -53,14 +53,14 @@ onMount(() => {
>
<div class="flex items-center transition text-black/75 dark:text-white/75 font-bold group-hover:text-(--primary) group-active:text-(--primary)">
{#if link.icon}
<Icon icon={link.icon} class="text-[1.1rem] mr-2" />
<SiteIcon icon={link.icon} class="text-[1.1rem] mr-2" />
{/if}
{link.name}
</div>
{#if !link.external}
<Icon icon="material-symbols:chevron-right-rounded" class="transition text-[1.25rem] text-(--primary)" />
<SiteIcon icon="material-symbols:chevron-right-rounded" class="transition text-[1.25rem] text-(--primary)" />
{:else}
<Icon icon="fa6-solid:arrow-up-right-from-square" class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" />
<SiteIcon icon="fa6-solid:arrow-up-right-from-square" class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" />
{/if}
</a>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { BREAKPOINT_LG } from "@constants/breakpoints";
import { getTranslateLanguageFromConfig, getSiteLanguage, setStoredLanguage, getDefaultLanguage } from "@/utils/language";
@@ -99,7 +99,7 @@ onDestroy(() => {
onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { togglePanel(); } }}
onmouseenter={openPanel}
>
<Icon icon="material-symbols:translate" class="text-[1.25rem] transition" />
<SiteIcon icon="material-symbols:translate" class="text-[1.25rem] transition" />
</button>
<!-- 翻译面板 -->
<div id="translate-panel-wrapper" class="fixed top-14.5 pt-5 right-4 w-[calc(100vw-2rem)] max-w-64 md:absolute md:top-11 md:right-0 md:w-64 md:pt-5 transition-all z-50" class:float-panel-closed={!isOpen}>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import { BREAKPOINT_LG } from "@/constants/breakpoints";
@@ -66,13 +66,13 @@ onMount(() => {
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={closePanel}>
<button aria-label="Wallpaper Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="wallpaper-mode-switch" onmouseenter={openPanel} onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { toggleWallpaperMode(); } }}>
<div class="absolute" class:opacity-0={mode !== WALLPAPER_BANNER}>
<Icon icon="material-symbols:image-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:image-outline" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== WALLPAPER_FULLSCREEN}>
<Icon icon="material-symbols:wallpaper" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:wallpaper" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== WALLPAPER_NONE}>
<Icon icon="material-symbols:hide-image-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:hide-image-outline" class="text-[1.25rem]"/>
</div>
</button>
<div id="wallpaper-mode-panel" class="absolute transition top-11 -right-2 pt-5" class:float-panel-closed={!isOpen}>
@@ -82,7 +82,7 @@ onMount(() => {
isLast={false}
onclick={() => switchWallpaperMode(WALLPAPER_BANNER)}
>
<Icon icon="material-symbols:image-outline" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:image-outline" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.wallpaperBanner)}
</DropdownItem>
<DropdownItem
@@ -90,7 +90,7 @@ onMount(() => {
isLast={false}
onclick={() => switchWallpaperMode(WALLPAPER_FULLSCREEN)}
>
<Icon icon="material-symbols:wallpaper" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:wallpaper" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.wallpaperFullscreen)}
</DropdownItem>
<DropdownItem
@@ -98,7 +98,7 @@ onMount(() => {
isLast={true}
onclick={() => switchWallpaperMode(WALLPAPER_NONE)}
>
<Icon icon="material-symbols:hide-image-outline" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:hide-image-outline" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.wallpaperNone)}
</DropdownItem>
</DropdownPanel>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import dayjs from 'dayjs';
import { BREAKPOINT_LG } from "@constants/breakpoints";
@@ -444,7 +444,7 @@
<button class="time-scale-select flex items-center gap-1">
{labels[timeScale]}
<span class="dropdown-icon flex items-center">
<Icon icon="material-symbols:keyboard-arrow-down-rounded" />
<SiteIcon icon="material-symbols:keyboard-arrow-down-rounded" />
</span>
</button>
<div class="dropdown-menu-custom">

View File

@@ -0,0 +1,137 @@
// AUTO-GENERATED by scripts/collect-icons.mjs — do not edit
export default {
"material-symbols": [
"archive",
"article",
"book",
"book-2-outline-rounded",
"calendar-today-outline-rounded",
"chevron-left-rounded",
"chevron-right-rounded",
"close",
"code",
"copyright-outline-rounded",
"dark-mode-outline-rounded",
"edit-calendar-outline-rounded",
"emoji-events",
"error",
"error-outline",
"event",
"expand-more",
"format-list-bulleted-rounded",
"graphic-eq",
"group",
"help-outline",
"hide-image-outline",
"history-rounded",
"home",
"home-rounded",
"image-outline",
"info",
"keyboard-arrow-down-rounded",
"keyboard-arrow-up-rounded",
"link",
"mail-outline",
"menu-rounded",
"more-horiz",
"movie",
"music-note",
"notes-rounded",
"palette-outline",
"pause",
"photo-library",
"play-arrow",
"progress-activity",
"psychology",
"queue-music",
"radio-button-partial-outline",
"repeat",
"repeat-one",
"rss-feed",
"schedule-outline-rounded",
"school",
"search-off",
"security",
"sentiment-sad",
"shuffle",
"skip-next",
"skip-previous",
"star",
"tag-rounded",
"timeline",
"translate",
"visibility-outline-rounded",
"volume-down",
"volume-off",
"volume-up",
"wallpaper",
"wb-sunny-outline-rounded",
"work"
],
"fa6-solid": [
"arrow-rotate-left",
"arrow-up-right-from-square",
"eye",
"xmark"
],
"fa6-brands": [
"creative-commons",
"git-alt",
"telegram"
],
"mdi": [
"car-outline",
"certificate-outline",
"pin"
],
"fa6-regular": [
"address-card"
],
"logos": [
"angular-icon",
"c-plusplus",
"css-3",
"docker-icon",
"git-icon",
"html-5",
"javascript",
"linux-tux",
"nestjs",
"nextjs-icon",
"openai-icon",
"python",
"react",
"typescript-icon",
"vue"
],
"skill-icons": [
"neovim-dark"
],
"vscode-icons": [
"file-type-cursorrules"
],
"devicon": [
"java",
"postgresql"
],
"material-icon-theme": [
"kotlin"
],
"hugeicons": [
"mentoring",
"office",
"test-tube"
],
"mingcute": [
"bridge-fill",
"robot-line",
"volleyball-fill",
"vue-fill"
],
"iconoir": [
"agile"
],
"game-icons": [
"podium-winner"
]
};

View File

@@ -0,0 +1,136 @@
{
"material-symbols": [
"archive",
"article",
"book",
"book-2-outline-rounded",
"calendar-today-outline-rounded",
"chevron-left-rounded",
"chevron-right-rounded",
"close",
"code",
"copyright-outline-rounded",
"dark-mode-outline-rounded",
"edit-calendar-outline-rounded",
"emoji-events",
"error",
"error-outline",
"event",
"expand-more",
"format-list-bulleted-rounded",
"graphic-eq",
"group",
"help-outline",
"hide-image-outline",
"history-rounded",
"home",
"home-rounded",
"image-outline",
"info",
"keyboard-arrow-down-rounded",
"keyboard-arrow-up-rounded",
"link",
"mail-outline",
"menu-rounded",
"more-horiz",
"movie",
"music-note",
"notes-rounded",
"palette-outline",
"pause",
"photo-library",
"play-arrow",
"progress-activity",
"psychology",
"queue-music",
"radio-button-partial-outline",
"repeat",
"repeat-one",
"rss-feed",
"schedule-outline-rounded",
"school",
"search-off",
"security",
"sentiment-sad",
"shuffle",
"skip-next",
"skip-previous",
"star",
"tag-rounded",
"timeline",
"translate",
"visibility-outline-rounded",
"volume-down",
"volume-off",
"volume-up",
"wallpaper",
"wb-sunny-outline-rounded",
"work"
],
"fa6-solid": [
"arrow-rotate-left",
"arrow-up-right-from-square",
"eye",
"xmark"
],
"fa6-brands": [
"creative-commons",
"git-alt",
"telegram"
],
"mdi": [
"car-outline",
"certificate-outline",
"pin"
],
"fa6-regular": [
"address-card"
],
"logos": [
"angular-icon",
"c-plusplus",
"css-3",
"docker-icon",
"git-icon",
"html-5",
"javascript",
"linux-tux",
"nestjs",
"nextjs-icon",
"openai-icon",
"python",
"react",
"typescript-icon",
"vue"
],
"skill-icons": [
"neovim-dark"
],
"vscode-icons": [
"file-type-cursorrules"
],
"devicon": [
"java",
"postgresql"
],
"material-icon-theme": [
"kotlin"
],
"hugeicons": [
"mentoring",
"office",
"test-tube"
],
"mingcute": [
"bridge-fill",
"robot-line",
"volleyball-fill",
"vue-fill"
],
"iconoir": [
"agile"
],
"game-icons": [
"podium-winner"
]
}

File diff suppressed because one or more lines are too long

5
src/global.d.ts vendored
View File

@@ -6,14 +6,9 @@ declare global {
}
interface Window {
// Define swup type directly since @swup/astro doesn't export AstroIntegration
swup: any;
semifullScrollHandler: (() => void) | null;
closeAnnouncement: () => void;
iconifyLoaded: boolean;
__iconifyLoader: {
load: () => Promise<void>;
};
translate?: {
service: {
use: (service: string) => void;

View File

@@ -8,7 +8,6 @@ import {
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT,
} from "@constants/constants";
import { widgetManager } from "@utils/widget";
import IconifyLoader from "@components/common/iconifyLoader.astro";
import Navbar from "@components/navbar.astro";
import FullscreenWallpaper from "@components/fullscreenWallpaper.astro";
import Banner from "@components/banner.astro";
@@ -88,9 +87,6 @@ const {
setOGTypeArticle={setOGTypeArticle}
postSlug={postSlug}
>
<!-- 全局图标加载器 -->
<IconifyLoader />
<!-- Navbar -->
<slot slot="head" name="head" />
<div

View File

@@ -13,7 +13,6 @@ import {
} from "@utils/skills";
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import IconifyLoader from "@components/common/iconifyLoader.astro";
import SkillCard from "@components/data/skillCard.astro";
import GridLayout from "@layouts/grid.astro";
import BackwardButton from "@components/backwardButton.astro";
@@ -76,13 +75,9 @@ const getCategoryText = (category: string) => {
}
};
// 收集所有技能图标用于预加载
const allIcons = skillsData.map((skill) => skill.icon).filter(Boolean);
---
<GridLayout title={title} description={subtitle}>
<!-- 图标加载器,预加载所有技能图标 -->
<IconifyLoader preloadIcons={allIcons} />
<div class="flex w-full rounded-(--radius-large) overflow-hidden relative min-h-32">
<div class="card-base z-10 px-9 py-6 relative w-full">
<BackwardButton currentPath={Astro.url.pathname} />
@@ -215,35 +210,4 @@ const allIcons = skillsData.map((skill) => skill.icon).filter(Boolean);
</div>
</div>
</div>
</GridLayout>
<script>
// 确保图标库已加载,并处理页面导航
document.addEventListener('DOMContentLoaded', () => {
// 如果图标加载器存在,确保图标已加载
if (window.__iconifyLoader) {
window.__iconifyLoader.load().then(() => {
// 图标加载完成后,触发技能卡片的重新渲染
const skillCards = document.querySelectorAll('.skill-card');
skillCards.forEach(card => {
card.dispatchEvent(new CustomEvent('iconify-ready'));
});
}).catch(error => {
console.error('Failed to load icons on skills page:', error);
});
}
});
// 处理页面导航时的图标重新加载
if (typeof window !== 'undefined') {
// 监听页面显示事件(包括前进/后退导航)
window.addEventListener('pageshow', (event) => {
if (event.persisted && window.__iconifyLoader) {
// 页面从缓存恢复,重新检查图标状态
setTimeout(() => {
window.__iconifyLoader.load().catch(console.error);
}, 100);
}
});
}
</script>
</GridLayout>

View File

@@ -13,7 +13,6 @@ import {
} from "@utils/timeline";
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import IconifyLoader from "@components/common/iconifyLoader.astro";
import TimelineItem from "@components/data/timelineItem.astro";
import GridLayout from "@layouts/grid.astro";
import BackwardButton from "@components/backwardButton.astro";
@@ -22,11 +21,6 @@ import BackwardButton from "@components/backwardButton.astro";
const title = LinkPresets[LinkPreset.Timeline].name;
const subtitle = LinkPresets[LinkPreset.Timeline].description;
// 收集所有时间线图标用于预加载
const allIcons = timelineData
.map((item) => item.icon || getTypeIcon(item.type))
.filter(Boolean);
// 获取时间线统计信息
const stats = getTimelineStats();
const currentItems = getCurrentItems();
@@ -53,8 +47,6 @@ const getTypeIcon = (type: string) => {
---
<GridLayout title={title} description={subtitle}>
<!-- 图标加载器,预加载所有时间线图标 -->
<IconifyLoader preloadIcons={allIcons} />
<div class="flex w-full rounded-(--radius-large) overflow-hidden relative min-h-32">
<div class="card-base z-10 px-9 py-6 relative w-full">
<BackwardButton currentPath={Astro.url.pathname} />
@@ -185,35 +177,4 @@ const getTypeIcon = (type: string) => {
</div>
</div>
</div>
</GridLayout>
<script>
// 确保图标库已加载,并处理页面导航
document.addEventListener('DOMContentLoaded', () => {
// 如果图标加载器存在,确保图标已加载
if (window.__iconifyLoader) {
window.__iconifyLoader.load().then(() => {
// 图标加载完成后,触发时间线项目的重新渲染
const timelineItems = document.querySelectorAll('.timeline-item');
timelineItems.forEach(item => {
item.dispatchEvent(new CustomEvent('iconify-ready'));
});
}).catch(error => {
console.error('Failed to load icons on timeline page:', error);
});
}
});
// 处理页面导航时的图标重新加载
if (typeof window !== 'undefined') {
// 监听页面显示事件(包括前进/后退导航)
window.addEventListener('pageshow', (event) => {
if (event.persisted && window.__iconifyLoader) {
// 页面从缓存恢复,重新检查图标状态
setTimeout(() => {
window.__iconifyLoader.load().catch(console.error);
}, 100);
}
});
}
</script>
</GridLayout>

View File

@@ -1,78 +0,0 @@
import { addCollection } from "@iconify/svelte/dist/functions.js";
import type { IconifyJSON } from "@iconify/types";
import devicon from "@iconify-json/devicon/icons.json";
import gameIcons from "@iconify-json/game-icons/icons.json";
import hugeicons from "@iconify-json/hugeicons/icons.json";
import iconoir from "@iconify-json/iconoir/icons.json";
import logos from "@iconify-json/logos/icons.json";
import materialIconTheme from "@iconify-json/material-icon-theme/icons.json";
import materialSymbols from "@iconify-json/material-symbols/icons.json";
import mdi from "@iconify-json/mdi/icons.json";
import mingcute from "@iconify-json/mingcute/icons.json";
import skillIcons from "@iconify-json/skill-icons/icons.json";
import vscodeIcons from "@iconify-json/vscode-icons/icons.json";
const collections: IconifyJSON[] = [
materialSymbols,
logos,
devicon,
hugeicons,
mingcute,
mdi,
skillIcons,
vscodeIcons,
materialIconTheme,
gameIcons,
iconoir,
];
let svelteRegistered = false;
let webComponentRegistered = false;
function addCollectionsToWebComponent(): void {
if (typeof window === "undefined" || webComponentRegistered) {
return;
}
const IconifyIcon = customElements.get("iconify-icon") as
| (CustomElementConstructor & {
addCollection?: (data: IconifyJSON, provider?: string) => boolean;
disableCache?: (storage: string) => void;
})
| undefined;
if (!IconifyIcon?.addCollection) {
return;
}
IconifyIcon.disableCache?.("all");
for (const collection of collections) {
IconifyIcon.addCollection(collection);
}
webComponentRegistered = true;
}
/** Register offline collections for @iconify/svelte (navbar, player, etc.). */
export function registerSvelteIconifyCollections(): void {
if (svelteRegistered) {
return;
}
svelteRegistered = true;
for (const collection of collections) {
addCollection(collection);
}
}
/** Register offline collections for iconify-icon web component (icon.astro). */
export function registerWebComponentIconifyCollections(): void {
addCollectionsToWebComponent();
}
/** Register all offline icon sets (Svelte + web component when available). */
export function registerIconifyCollections(): void {
registerSvelteIconifyCollections();
registerWebComponentIconifyCollections();
}