mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-07-28 19:35:51 +04:00
Cut iconify
This commit is contained in:
16
src/components/common/SiteIcon.svelte
Normal file
16
src/components/common/SiteIcon.svelte
Normal 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>
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
137
src/generated/icon-include.mjs
Normal file
137
src/generated/icon-include.mjs
Normal 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"
|
||||
]
|
||||
};
|
||||
136
src/generated/icon-inventory.json
Normal file
136
src/generated/icon-inventory.json
Normal 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"
|
||||
]
|
||||
}
|
||||
113
src/generated/site-icon-svgs.ts
Normal file
113
src/generated/site-icon-svgs.ts
Normal file
File diff suppressed because one or more lines are too long
5
src/global.d.ts
vendored
5
src/global.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user