mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-07-28 19:35:51 +04:00
Cut iconify
This commit is contained in:
@@ -31,6 +31,28 @@ import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
|
||||
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
|
||||
import { remarkMermaid } from "./src/plugins/remark-mermaid.js";
|
||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import iconInclude from "./src/generated/icon-include.mjs";
|
||||
|
||||
const iconIncludeFile = fileURLToPath(
|
||||
new URL("./src/generated/icon-include.mjs", import.meta.url),
|
||||
);
|
||||
|
||||
/** Restart dev server when collect-icons updates astro-icon include */
|
||||
function iconIncludeHmr() {
|
||||
return {
|
||||
name: "icon-include-hmr",
|
||||
configureServer(server) {
|
||||
server.watcher.add(iconIncludeFile);
|
||||
server.watcher.on("change", (file) => {
|
||||
if (file === iconIncludeFile) {
|
||||
console.log("[icons] icon-include.mjs changed, restarting dev server…");
|
||||
server.restart();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// https://astro.build/config
|
||||
@@ -75,12 +97,7 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
icon({
|
||||
include: {
|
||||
"fa6-brands": ["*"],
|
||||
"fa6-regular": ["*"],
|
||||
"fa6-solid": ["*"],
|
||||
mdi: ["*"],
|
||||
},
|
||||
include: iconInclude,
|
||||
}),
|
||||
expressiveCode({
|
||||
themes: ["github-light", "github-dark"],
|
||||
@@ -191,7 +208,7 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
plugins: [tailwindcss(), iconIncludeHmr()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
onwarn(warning, warn) {
|
||||
|
||||
13
package.json
13
package.json
@@ -9,9 +9,10 @@
|
||||
"lint": "biome check --write ./src",
|
||||
"format": "biome format --write ./src",
|
||||
"check": "astro check",
|
||||
"dev": "astro dev",
|
||||
"build": "node scripts/copy-iconify.cjs && astro build",
|
||||
"copy-iconify": "node scripts/copy-iconify.cjs",
|
||||
"dev": "node scripts/collect-icons.mjs && node scripts/generate-site-icons.mjs && astro dev",
|
||||
"icons:collect": "node scripts/collect-icons.mjs",
|
||||
"icons:generate": "node scripts/generate-site-icons.mjs",
|
||||
"build": "node scripts/collect-icons.mjs && node scripts/generate-site-icons.mjs && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"new-post": "node scripts/new-post.js"
|
||||
@@ -44,8 +45,6 @@
|
||||
"@iconify-json/mingcute": "^1.2.5",
|
||||
"@iconify-json/skill-icons": "^1.2.3",
|
||||
"@iconify-json/vscode-icons": "^1.2.40",
|
||||
"@iconify/utils": "^2.3.0",
|
||||
"@iconify/svelte": "^5.2.1",
|
||||
"@swup/astro": "^1.7.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
@@ -65,7 +64,6 @@
|
||||
"marked": "^17.0.1",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"node-html-parser": "^7.0.2",
|
||||
"iconify-icon": "1.0.7",
|
||||
"overlayscrollbars": "^2.14.0",
|
||||
"photoswipe": "^5.4.4",
|
||||
"reading-time": "^1.5.0",
|
||||
@@ -95,7 +93,8 @@
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"postcss-import": "^16.1.1"
|
||||
"postcss-import": "^16.1.1",
|
||||
"@iconify/utils": "^2.3.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.14.4"
|
||||
}
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
@@ -89,12 +89,6 @@ importers:
|
||||
'@iconify-json/vscode-icons':
|
||||
specifier: ^1.2.40
|
||||
version: 1.2.50
|
||||
'@iconify/svelte':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(svelte@5.49.1)
|
||||
'@iconify/utils':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@swup/astro':
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0
|
||||
@@ -134,9 +128,6 @@ importers:
|
||||
hastscript:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
iconify-icon:
|
||||
specifier: 1.0.7
|
||||
version: 1.0.7
|
||||
js-yaml:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
@@ -222,6 +213,9 @@ importers:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.3.13
|
||||
version: 2.3.13
|
||||
'@iconify/utils':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@rollup/plugin-yaml':
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2(rollup@2.79.2)
|
||||
@@ -1395,11 +1389,6 @@ packages:
|
||||
'@iconify-json/vscode-icons@1.2.50':
|
||||
resolution: {integrity: sha512-XqG9nO1m+IiY8/RvT5BJjM/eG3CDRXBGRRPqR/MAcVi734yZ0Pa+jqObshZfnpUeIsuDKNn9C/7ZW00ART6kog==}
|
||||
|
||||
'@iconify/svelte@5.2.1':
|
||||
resolution: {integrity: sha512-zHmsIPmnIhGd5gc95bNN5FL+GifwMZv7M2rlZEpa7IXYGFJm/XGHdWf6PWQa6OBoC+R69WyiPO9NAj5wjfjbow==}
|
||||
peerDependencies:
|
||||
svelte: '>5.0.0'
|
||||
|
||||
'@iconify/tools@4.2.0':
|
||||
resolution: {integrity: sha512-WRxPva/ipxYkqZd1+CkEAQmd86dQmrwH0vwK89gmp2Kh2WyyVw57XbPng0NehP3x4V1LzLsXUneP1uMfTMZmUA==}
|
||||
|
||||
@@ -3369,9 +3358,6 @@ packages:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
iconify-icon@1.0.7:
|
||||
resolution: {integrity: sha512-MxaO3Jhf3f5ymPWGHR9x74f90TNKcq1D+B2iGucGhVtqAgbC9EtM06kKiTGH2CKELNnexckwhrA3/+OpT4HKFw==}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6949,11 +6935,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/svelte@5.2.1(svelte@5.49.1)':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
svelte: 5.49.1
|
||||
|
||||
'@iconify/tools@4.2.0':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
@@ -9199,10 +9180,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
iconify-icon@1.0.7:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
12
public/assets/iconify/iconify-icon.min.js
vendored
12
public/assets/iconify/iconify-icon.min.js
vendored
File diff suppressed because one or more lines are too long
135
scripts/collect-icons.mjs
Normal file
135
scripts/collect-icons.mjs
Normal file
@@ -0,0 +1,135 @@
|
||||
import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
|
||||
import { join, relative } from "path";
|
||||
|
||||
const ROOT = join(import.meta.dirname, "..");
|
||||
const SRC = join(ROOT, "src");
|
||||
const OUT_DIR = join(SRC, "generated");
|
||||
const OUT_FILE = join(OUT_DIR, "icon-inventory.json");
|
||||
const OUT_INCLUDE = join(OUT_DIR, "icon-include.mjs");
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const ICON_ALIASES = {
|
||||
"eos-icons:loading": "material-symbols:progress-activity",
|
||||
};
|
||||
|
||||
const KNOWN_PREFIXES = new Set([
|
||||
"material-symbols",
|
||||
"fa6-brands",
|
||||
"fa6-regular",
|
||||
"fa6-solid",
|
||||
"mdi",
|
||||
"logos",
|
||||
"devicon",
|
||||
"hugeicons",
|
||||
"mingcute",
|
||||
"skill-icons",
|
||||
"vscode-icons",
|
||||
"material-icon-theme",
|
||||
"game-icons",
|
||||
"iconoir",
|
||||
]);
|
||||
|
||||
const ICON_PATTERN =
|
||||
/(?:name|icon)=["'{]([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)/gi;
|
||||
|
||||
const JSON_ICON_PATTERN = /"icon"\s*:\s*"([^"]+)"/g;
|
||||
const YAML_ICON_PATTERN = /\bicon:\s*["']([^"']+)["']/g;
|
||||
const TS_ICON_PATTERN = /\bicon\s*:\s*["']([^"']+)["']/g;
|
||||
const RETURN_ICON_PATTERN =
|
||||
/return\s+["']([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)["']/g;
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) {
|
||||
walk(full, files);
|
||||
} else {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function normalizeIcon(name) {
|
||||
const trimmed = name.trim();
|
||||
return ICON_ALIASES[trimmed] ?? trimmed;
|
||||
}
|
||||
|
||||
function addIcon(icons, raw) {
|
||||
const name = normalizeIcon(raw);
|
||||
if (!name.includes(":")) return;
|
||||
const [prefix, iconName] = name.split(":");
|
||||
if (!prefix || !iconName || !KNOWN_PREFIXES.has(prefix)) return;
|
||||
if (!icons[prefix]) icons[prefix] = new Set();
|
||||
icons[prefix].add(iconName);
|
||||
}
|
||||
|
||||
function scanFile(path, icons) {
|
||||
const content = readFileSync(path, "utf8");
|
||||
const rel = relative(ROOT, path).replace(/\\/g, "/");
|
||||
|
||||
if (rel.endsWith(".json")) {
|
||||
for (const match of content.matchAll(JSON_ICON_PATTERN)) {
|
||||
addIcon(icons, match[1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (rel.endsWith(".yaml") || rel.endsWith(".yml")) {
|
||||
for (const match of content.matchAll(YAML_ICON_PATTERN)) {
|
||||
addIcon(icons, match[1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/\.(astro|svelte|ts|tsx|js|mjs)$/.test(rel)) return;
|
||||
|
||||
for (const match of content.matchAll(ICON_PATTERN)) {
|
||||
addIcon(icons, match[1]);
|
||||
}
|
||||
for (const match of content.matchAll(TS_ICON_PATTERN)) {
|
||||
addIcon(icons, match[1]);
|
||||
}
|
||||
for (const match of content.matchAll(RETURN_ICON_PATTERN)) {
|
||||
addIcon(icons, match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
/** @type {Record<string, Set<string>>} */
|
||||
const icons = {};
|
||||
|
||||
const files = walk(SRC);
|
||||
for (const file of files) {
|
||||
if (file.includes("generated")) continue;
|
||||
scanFile(file, icons);
|
||||
}
|
||||
|
||||
const extraConfigFiles = [join(ROOT, "twilight.config.yaml")];
|
||||
for (const file of extraConfigFiles) {
|
||||
try {
|
||||
scanFile(file, icons);
|
||||
} catch {
|
||||
/* optional config */
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, string[]>} */
|
||||
const inventory = {};
|
||||
for (const [prefix, names] of Object.entries(icons)) {
|
||||
inventory[prefix] = [...names].sort();
|
||||
}
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
writeFileSync(OUT_FILE, `${JSON.stringify(inventory, null, 2)}\n`, "utf8");
|
||||
writeFileSync(
|
||||
OUT_INCLUDE,
|
||||
`// AUTO-GENERATED by scripts/collect-icons.mjs — do not edit\nexport default ${JSON.stringify(inventory, null, 4)};\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const total = Object.values(inventory).reduce((n, arr) => n + arr.length, 0);
|
||||
console.log(`Collected ${total} icons in ${Object.keys(inventory).length} sets -> ${relative(ROOT, OUT_FILE)}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,9 +0,0 @@
|
||||
const { copyFileSync, mkdirSync } = require("fs");
|
||||
const { dirname, join } = require("path");
|
||||
|
||||
const src = join(__dirname, "../node_modules/iconify-icon/dist/iconify-icon.min.js");
|
||||
const dest = join(__dirname, "../public/assets/iconify/iconify-icon.min.js");
|
||||
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
copyFileSync(src, dest);
|
||||
console.log(`Copied iconify-icon.min.js to ${dest}`);
|
||||
92
scripts/generate-site-icons.mjs
Normal file
92
scripts/generate-site-icons.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { createRequire } from "module";
|
||||
import { getIconData, iconToSVG } from "@iconify/utils";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const ROOT = join(import.meta.dirname, "..");
|
||||
const INVENTORY_FILE = join(ROOT, "src/generated/icon-inventory.json");
|
||||
const OUT_DIR = join(ROOT, "src/generated");
|
||||
const OUT_FILE = join(OUT_DIR, "site-icon-svgs.ts");
|
||||
|
||||
const PACKAGE_BY_PREFIX = {
|
||||
"fa6-brands": "@iconify-json/fa6-brands",
|
||||
"fa6-regular": "@iconify-json/fa6-regular",
|
||||
"fa6-solid": "@iconify-json/fa6-solid",
|
||||
mdi: "@iconify-json/mdi",
|
||||
"material-symbols": "@iconify-json/material-symbols",
|
||||
logos: "@iconify-json/logos",
|
||||
devicon: "@iconify-json/devicon",
|
||||
hugeicons: "@iconify-json/hugeicons",
|
||||
mingcute: "@iconify-json/mingcute",
|
||||
"skill-icons": "@iconify-json/skill-icons",
|
||||
"vscode-icons": "@iconify-json/vscode-icons",
|
||||
"material-icon-theme": "@iconify-json/material-icon-theme",
|
||||
"game-icons": "@iconify-json/game-icons",
|
||||
iconoir: "@iconify-json/iconoir",
|
||||
};
|
||||
|
||||
function loadIconSet(prefix) {
|
||||
const pkg = PACKAGE_BY_PREFIX[prefix];
|
||||
if (!pkg) return null;
|
||||
return require(`${pkg}/icons.json`);
|
||||
}
|
||||
|
||||
function buildSvg(iconName) {
|
||||
const [prefix, name] = iconName.split(":");
|
||||
const iconSet = loadIconSet(prefix);
|
||||
if (!iconSet) {
|
||||
console.warn(`Unknown icon set: ${prefix} (${iconName})`);
|
||||
return null;
|
||||
}
|
||||
const data = getIconData(iconSet, name);
|
||||
if (!data) {
|
||||
console.warn(`Icon not found: ${iconName}`);
|
||||
return null;
|
||||
}
|
||||
const { body, attributes } = iconToSVG(data, { height: "1em" });
|
||||
const attrs = Object.entries(attributes)
|
||||
.map(([k, v]) => `${k}="${v}"`)
|
||||
.join(" ");
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${body}</svg>`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const inventory = JSON.parse(readFileSync(INVENTORY_FILE, "utf8"));
|
||||
/** @type {Record<string, string>} */
|
||||
const svgs = {};
|
||||
|
||||
for (const [prefix, names] of Object.entries(inventory)) {
|
||||
for (const name of names) {
|
||||
const fullName = `${prefix}:${name}`;
|
||||
const svg = buildSvg(fullName);
|
||||
if (svg) svgs[fullName] = svg;
|
||||
}
|
||||
}
|
||||
|
||||
const fallback =
|
||||
svgs["material-symbols:help-outline"] ??
|
||||
svgs["material-symbols:info"] ??
|
||||
Object.values(svgs)[0] ??
|
||||
"";
|
||||
|
||||
const lines = [
|
||||
"// Generated by scripts/generate-site-icons.mjs — do not edit manually",
|
||||
"",
|
||||
`export const siteIconFallback = ${JSON.stringify(fallback)};`,
|
||||
"",
|
||||
"export const siteIconSvgs: Record<string, string> = {",
|
||||
];
|
||||
|
||||
for (const [name, svg] of Object.entries(svgs).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(svg)},`);
|
||||
}
|
||||
|
||||
lines.push("};", "");
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
writeFileSync(OUT_FILE, `${lines.join("\n")}\n`, "utf8");
|
||||
console.log(`Generated ${Object.keys(svgs).length} SVGs -> ${OUT_FILE}`);
|
||||
}
|
||||
|
||||
main();
|
||||
17
scripts/icon-inventory-include.mjs
Normal file
17
scripts/icon-inventory-include.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const inventoryPath = join(import.meta.dirname, "../src/generated/icon-inventory.json");
|
||||
|
||||
export function loadIconInventory() {
|
||||
return JSON.parse(readFileSync(inventoryPath, "utf8"));
|
||||
}
|
||||
|
||||
export function toAstroIconInclude(inventory) {
|
||||
/** @type {Record<string, string[]>} */
|
||||
const include = {};
|
||||
for (const [prefix, icons] of Object.entries(inventory)) {
|
||||
include[prefix] = icons;
|
||||
}
|
||||
return include;
|
||||
}
|
||||
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} />
|
||||
@@ -216,34 +211,3 @@ const allIcons = skillsData.map((skill) => skill.icon).filter(Boolean);
|
||||
</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>
|
||||
@@ -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} />
|
||||
@@ -186,34 +178,3 @@ const getTypeIcon = (type: string) => {
|
||||
</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>
|
||||
@@ -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