2892 lines
191 KiB
HTML
2892 lines
191 KiB
HTML
<!DOCTYPE html><html lang="zh-CN" class="bg-[var(--page-bg)] text-[14px] md:text-[16px]" data-overlayscrollbars-initialize data-astro-cid-sckkx6r4 style="--bannerOffset: 15vh;--banner-height-home: 65vh;--banner-height: 35vh;--configHue: 210;--page-width: 90rem;"> <head><title>技能展示 - FutureOSS Docs</title><meta charset="UTF-8"><meta name="description" content="我的技术技能和专业知识"><meta name="author" content="FutureOSS"><meta property="og:site_name" content="FutureOSS Docs"><meta property="og:url" content="https://oss-runtime.dev/blog/skills/"><meta property="og:title" content="技能展示 - FutureOSS Docs"><meta property="og:description" content="我的技术技能和专业知识"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><meta property="twitter:url" content="https://oss-runtime.dev/blog/skills/"><meta name="twitter:title" content="技能展示 - FutureOSS Docs"><meta name="twitter:description" content="我的技术技能和专业知识"><meta name="viewport" content="width=device-width"><meta name="generator" content="Astro v5.13.2"><link rel="icon" href="/blog/favicon/favicon-light-32.png" sizes="32x32" media="(prefers-color-scheme: light)"><link rel="icon" href="/blog/favicon/favicon-light-128.png" sizes="128x128" media="(prefers-color-scheme: light)"><link rel="icon" href="/blog/favicon/favicon-light-180.png" sizes="180x180" media="(prefers-color-scheme: light)"><link rel="icon" href="/blog/favicon/favicon-light-192.png" sizes="192x192" media="(prefers-color-scheme: light)"><link rel="icon" href="/blog/favicon/favicon-dark-32.png" sizes="32x32" media="(prefers-color-scheme: dark)"><link rel="icon" href="/blog/favicon/favicon-dark-128.png" sizes="128x128" media="(prefers-color-scheme: dark)"><link rel="icon" href="/blog/favicon/favicon-dark-180.png" sizes="180x180" media="(prefers-color-scheme: dark)"><link rel="icon" href="/blog/favicon/favicon-dark-192.png" sizes="192x192" media="(prefers-color-scheme: dark)"><!-- Set the theme before the page is rendered to avoid a flash --><script>(function(){const DEFAULT_THEME = "auto";
|
||
const LIGHT_MODE = "light";
|
||
const DARK_MODE = "dark";
|
||
const AUTO_MODE = "auto";
|
||
const BANNER_HEIGHT_EXTEND = 30;
|
||
const PAGE_WIDTH = 90;
|
||
const configHue = 210;
|
||
|
||
// Load the theme from local storage
|
||
const theme = localStorage.getItem('theme') || DEFAULT_THEME;
|
||
switch (theme) {
|
||
case LIGHT_MODE:
|
||
document.documentElement.classList.remove('dark');
|
||
break
|
||
case DARK_MODE:
|
||
document.documentElement.classList.add('dark');
|
||
break
|
||
case AUTO_MODE:
|
||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
document.documentElement.classList.add('dark');
|
||
} else {
|
||
document.documentElement.classList.remove('dark');
|
||
}
|
||
}
|
||
|
||
// Load the hue from local storage
|
||
const hue = localStorage.getItem('hue') || configHue;
|
||
document.documentElement.style.setProperty('--hue', hue);
|
||
|
||
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
|
||
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
|
||
offset = offset - offset % 4;
|
||
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
|
||
})();</script><!-- defines global css variables. This will be applied to <html> <body> and some other elements idk why --><link rel="alternate" type="application/rss+xml" title="FutureOSS" href="https://oss-runtime.dev/rss.xml"><link rel="stylesheet" href="/blog/_astro/Layout.DSulWsr7.css">
|
||
<link rel="stylesheet" href="/blog/_astro/Layout.CKPtf-VR.css">
|
||
<link rel="stylesheet" href="/blog/_astro/_page_.BQ6XwRvy.css">
|
||
<style>.line-clamp-2[data-astro-cid-xahix5fp]{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
</style>
|
||
<link rel="stylesheet" href="/blog/_astro/_page_.QUsIdifj.css">
|
||
<link rel="stylesheet" href="/blog/_astro/_page_.D6v8Siar.css">
|
||
<link rel="stylesheet" href="/blog/_astro/about.DhJwTaWu.css">
|
||
<style>.card-base[data-astro-cid-fepk5tcl]{background:var(--card-bg);border:1px solid var(--line-divider)}
|
||
.card-base[data-astro-cid-pyfuvgrx]{background:var(--card-bg);border:1px solid var(--line-divider);transition:all .3s ease}.moments-header[data-astro-cid-pyfuvgrx]{padding:1rem;border-radius:8px;background:linear-gradient(135deg,var(--primary) 0%,var(--primary-dark, var(--primary)) 100%);color:#fff;position:relative;overflow:hidden}.header-content[data-astro-cid-pyfuvgrx]{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.moments-title[data-astro-cid-pyfuvgrx]{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.1)}.moments-subtitle[data-astro-cid-pyfuvgrx]{color:#ffffffe6}.stat-number[data-astro-cid-pyfuvgrx]{color:#fff}.stat-label[data-astro-cid-pyfuvgrx]{color:#fffc}.image-item[data-astro-cid-pyfuvgrx] img[data-astro-cid-pyfuvgrx]{transition:transform .3s ease}.image-item[data-astro-cid-pyfuvgrx]:hover img[data-astro-cid-pyfuvgrx]{transform:scale(1.05)}.action-btn[data-astro-cid-pyfuvgrx]{background:none;border:none;cursor:pointer;color:var(--text-muted)}.action-btn[data-astro-cid-pyfuvgrx]:hover{color:var(--primary)}@media(max-width:640px){.moments-header[data-astro-cid-pyfuvgrx]{padding:.75rem}.header-content[data-astro-cid-pyfuvgrx]{flex-direction:column;text-align:center;gap:.75rem}.moment-images[data-astro-cid-pyfuvgrx]{grid-template-columns:repeat(2,1fr)}.moment-footer[data-astro-cid-pyfuvgrx]{flex-direction:column;align-items:flex-start;gap:.5rem}}@media(min-width:641px)and (max-width:900px){.moments-header[data-astro-cid-pyfuvgrx]{padding:1.25rem}.header-content[data-astro-cid-pyfuvgrx]{display:flex;justify-content:space-between;align-items:center}.moment-item[data-astro-cid-pyfuvgrx]{padding:1.5rem}.moment-images[data-astro-cid-pyfuvgrx]{grid-template-columns:repeat(3,1fr);gap:.75rem;max-width:500px}.moment-text[data-astro-cid-pyfuvgrx]{font-size:1rem;line-height:1.7}.moment-footer[data-astro-cid-pyfuvgrx]{margin-top:1rem}}@media(min-width:901px){.moments-header[data-astro-cid-pyfuvgrx]{padding:1.5rem}.moment-item[data-astro-cid-pyfuvgrx]{padding:2rem}.moment-images[data-astro-cid-pyfuvgrx]{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));max-width:600px;gap:1rem}.moment-text[data-astro-cid-pyfuvgrx]{font-size:1.1rem;line-height:1.8}}@media(max-width:480px){.card-base[data-astro-cid-pyfuvgrx]{margin:0 -.5rem}.moment-item[data-astro-cid-pyfuvgrx]{border-radius:8px}.moments-header[data-astro-cid-pyfuvgrx]{border-radius:6px}}
|
||
.card-base[data-astro-cid-sahthylw]{border-radius:var(--radius-large);border-width:1px;border-color:var(--line-divider);background-color:var(--card-bg);--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.gallery-group[data-astro-cid-sahthylw].expanded{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.gallery-content[data-astro-cid-sahthylw]{max-height:0;overflow:hidden;transition:max-height .3s ease-out}.gallery-group[data-astro-cid-sahthylw].expanded .gallery-content[data-astro-cid-sahthylw]{max-height:2000px}.gallery-image[data-astro-cid-sahthylw]:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}@media(max-width:768px){.gallery-content[data-astro-cid-sahthylw] .grid[data-astro-cid-sahthylw]{grid-template-columns:repeat(2,1fr)}}@media(max-width:480px){.gallery-content[data-astro-cid-sahthylw] .grid[data-astro-cid-sahthylw]{grid-template-columns:repeat(1,1fr)}}
|
||
.line-clamp-1[data-astro-cid-aid3sr62]{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.line-clamp-2[data-astro-cid-aid3sr62]{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.line-clamp-2[data-astro-cid-qlh7ngej]{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
</style>
|
||
<link rel="stylesheet" href="/blog/_astro/_page_.C0I9pl0C.css"><script type="module" src="/blog/_astro/page.OjjRkIOC.js"></script></head> <body class=" min-h-screen enable-banner" data-overlayscrollbars-initialize data-astro-cid-sckkx6r4 style="--bannerOffset: 15vh;--banner-height-home: 65vh;--banner-height: 35vh;--configHue: 210;--page-width: 90rem;"> <!-- 页面顶部渐变高光效果 - 只在full和semifull模式下显示 --> <div class="top-gradient-highlight" data-astro-cid-sckkx6r4 style="--bannerOffset: 15vh;--banner-height-home: 65vh;--banner-height: 35vh;--configHue: 210;--page-width: 90rem;"></div> <div id="config-carrier" data-hue="210"></div> <!-- Iconify图标库加载器 --><script>(function(){const preloadIcons = [];
|
||
const timeout = 10000;
|
||
const retryCount = 3;
|
||
|
||
// 全局图标加载逻辑
|
||
(function() {
|
||
'use strict';
|
||
|
||
// 避免重复加载
|
||
if (window.__iconifyLoaderInitialized) {
|
||
return;
|
||
}
|
||
window.__iconifyLoaderInitialized = true;
|
||
|
||
// 图标加载器类
|
||
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 = timeout, retryCount: maxRetries = retryCount } = 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(timeout, retryCount) {
|
||
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
||
try {
|
||
await this.loadScript(timeout);
|
||
return;
|
||
} catch (error) {
|
||
console.warn(`Iconify load attempt ${attempt} failed:`, error);
|
||
|
||
if (attempt === retryCount) {
|
||
throw new Error(`Failed to load Iconify after ${retryCount} attempts`);
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
}
|
||
}
|
||
|
||
loadScript(timeout) {
|
||
return new Promise((resolve, reject) => {
|
||
// 检查是否已经存在
|
||
if (this.isIconifyReady()) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const existingScript = document.querySelector('script[src*="iconify-icon"]');
|
||
if (existingScript) {
|
||
this.waitForIconifyReady().then(resolve).catch(reject);
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js';
|
||
script.async = true;
|
||
script.crossOrigin = 'anonymous';
|
||
|
||
const timeoutId = setTimeout(() => {
|
||
script.remove();
|
||
reject(new Error('Script load timeout'));
|
||
}, timeout);
|
||
|
||
script.onload = () => {
|
||
clearTimeout(timeoutId);
|
||
this.waitForIconifyReady().then(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() {
|
||
if (this.preloadQueue.size === 0) return;
|
||
|
||
const iconsToLoad = Array.from(this.preloadQueue);
|
||
this.preloadQueue.clear();
|
||
|
||
await this.preloadIcons(iconsToLoad);
|
||
}
|
||
|
||
async preloadIcons(icons) {
|
||
if (!this.isLoaded || icons.length === 0) return;
|
||
|
||
return new Promise((resolve) => {
|
||
let loadedCount = 0;
|
||
const totalIcons = icons.length;
|
||
const tempElements = [];
|
||
|
||
const cleanup = () => {
|
||
tempElements.forEach(el => {
|
||
if (el.parentNode) {
|
||
el.parentNode.removeChild(el);
|
||
}
|
||
});
|
||
};
|
||
|
||
const checkComplete = () => {
|
||
loadedCount++;
|
||
if (loadedCount >= totalIcons) {
|
||
cleanup();
|
||
resolve();
|
||
}
|
||
};
|
||
|
||
icons.forEach(icon => {
|
||
const tempIcon = document.createElement('iconify-icon');
|
||
tempIcon.setAttribute('icon', icon);
|
||
tempIcon.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;';
|
||
|
||
tempIcon.addEventListener('load', checkComplete);
|
||
tempIcon.addEventListener('error', checkComplete);
|
||
|
||
tempElements.push(tempIcon);
|
||
document.body.appendChild(tempIcon);
|
||
});
|
||
|
||
// 设置超时清理
|
||
setTimeout(() => {
|
||
cleanup();
|
||
resolve();
|
||
}, 3000);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 创建全局实例
|
||
window.__iconifyLoader = new IconifyLoader();
|
||
|
||
// 立即开始加载
|
||
window.__iconifyLoader.load().catch(error => {
|
||
console.error('Failed to initialize Iconify:', error);
|
||
});
|
||
|
||
// 如果有预加载图标,添加到队列
|
||
if (preloadIcons && 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> <!-- 为不支持JavaScript的情况提供备用方案 --><noscript><style>
|
||
iconify-icon {
|
||
display: none;
|
||
}
|
||
.icon-fallback {
|
||
display: inline-block;
|
||
}
|
||
</style></noscript> <div id="top-row" class="z-50 pointer-events-none relative transition-all duration-700 max-w-[var(--page-width)] px-0 md:px-4 mx-auto" data-astro-cid-haiuh7kc> <div id="navbar-wrapper" class="pointer-events-auto sticky top-0 transition-all" data-astro-cid-haiuh7kc> <div id="navbar" class="z-50 onload-animation" data-transparent-mode="semifull" data-is-home="false"> <div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation --> <div class="!overflow-visible max-w-[var(--page-width)] h-[4.5rem] mx-auto flex items-center justify-between px-4"> <a href="/blog/" class="btn-plain scale-animation rounded-lg h-[3.25rem] px-5 font-bold active:scale-95"> <div class="flex flex-row text-[var(--primary)] items-center text-md"> <svg width="1em" height="1em" class="text-[1.75rem] mb-1 mr-2" data-icon="material-symbols:home-outline-rounded"> <symbol id="ai:material-symbols:home-outline-rounded" viewBox="0 0 24 24"><path fill="currentColor" d="M6 19h3v-5q0-.425.288-.712T10 13h4q.425 0 .713.288T15 14v5h3v-9l-6-4.5L6 10zm-2 0v-9q0-.475.213-.9t.587-.7l6-4.5q.525-.4 1.2-.4t1.2.4l6 4.5q.375.275.588.7T20 10v9q0 .825-.588 1.413T18 21h-4q-.425 0-.712-.288T13 20v-5h-2v5q0 .425-.288.713T10 21H6q-.825 0-1.412-.587T4 19m8-6.75"/></symbol><use href="#ai:material-symbols:home-outline-rounded"></use> </svg> FutureOSS Docs </div> </a> <div class="hidden md:flex items-center space-x-1"> <div class="dropdown-container" data-dropdown data-astro-cid-qou34bit> <a aria-label="主页" href="/blog/" class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95" data-astro-cid-qou34bit> <div class="flex items-center" data-astro-cid-qou34bit> <svg width="1em" height="1em" class="text-[1.1rem] mr-2" data-astro-cid-qou34bit="true" data-icon="material-symbols:home"> <symbol id="ai:material-symbols:home" viewBox="0 0 24 24"><path fill="currentColor" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7z"/></symbol><use href="#ai:material-symbols:home"></use> </svg> 主页 </div> </a> </div> <script type="module">document.addEventListener("DOMContentLoaded",function(){const i=document.querySelectorAll("[data-dropdown]");i.forEach(t=>{const e=t.querySelector("[data-dropdown-trigger]"),o=t.querySelector("[data-dropdown-menu]"),n=t.querySelectorAll(".dropdown-item");!e||!o||(e.addEventListener("keydown",function(r){r.key==="Enter"||r.key===" "?(r.preventDefault(),d(t,e,o)):r.key==="ArrowDown"?(r.preventDefault(),u(t,e,o),n.length>0&&n[0].focus()):r.key==="Escape"&&a(t,e,o)}),n.forEach((r,l)=>{r.addEventListener("keydown",function(s){if(s.key==="ArrowDown"){s.preventDefault();const c=(l+1)%n.length;n[c].focus()}else if(s.key==="ArrowUp"){s.preventDefault();const c=(l-1+n.length)%n.length;n[c].focus()}else s.key==="Escape"&&(a(t,e,o),e.focus())})}))}),document.addEventListener("click",function(t){i.forEach(e=>{if(!e.contains(t.target)){const o=e.querySelector("[data-dropdown-trigger]"),n=e.querySelector("[data-dropdown-menu]");o&&n&&a(e,o,n)}})})});function d(i,t,e){t.getAttribute("aria-expanded")==="true"?a(i,t,e):u(i,t,e)}function u(i,t,e){t.setAttribute("aria-expanded","true"),e.classList.remove("opacity-0","invisible","pointer-events-none","translate-y-[-8px]"),e.classList.add("opacity-100","visible","pointer-events-auto","translate-y-0")}function a(i,t,e){t.setAttribute("aria-expanded","false"),e.classList.add("opacity-0","invisible","pointer-events-none","translate-y-[-8px]"),e.classList.remove("opacity-100","visible","pointer-events-auto","translate-y-0")}</script><div class="dropdown-container" data-dropdown data-astro-cid-qou34bit> <a aria-label="归档" href="/blog/archive/" class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95" data-astro-cid-qou34bit> <div class="flex items-center" data-astro-cid-qou34bit> <svg width="1em" height="1em" class="text-[1.1rem] mr-2" data-astro-cid-qou34bit="true" data-icon="material-symbols:archive"> <symbol id="ai:material-symbols:archive" viewBox="0 0 24 24"><path fill="currentColor" d="m12 18l4-4l-1.4-1.4l-1.6 1.6V10h-2v4.2l-1.6-1.6L8 14zm-7 3q-.825 0-1.412-.587T3 19V6.525q0-.35.113-.675t.337-.6L4.7 3.725q.275-.35.687-.538T6.25 3h11.5q.45 0 .863.188t.687.537l1.25 1.525q.225.275.338.6t.112.675V19q0 .825-.587 1.413T19 21zm.4-15h13.2l-.85-1H6.25z"/></symbol><use href="#ai:material-symbols:archive"></use> </svg> 归档 </div> </a> </div> <div class="dropdown-container" data-dropdown data-astro-cid-qou34bit> <button class="btn-plain scale-animation rounded-lg h-11 font-bold px-5 active:scale-95 dropdown-trigger" aria-expanded="false" aria-haspopup="true" data-dropdown-trigger data-astro-cid-qou34bit> <div class="flex items-center" data-astro-cid-qou34bit> <svg width="1em" height="1em" class="text-[1.1rem] mr-2" data-astro-cid-qou34bit="true" data-icon="material-symbols:code"> <symbol id="ai:material-symbols:code" viewBox="0 0 24 24"><path fill="currentColor" d="m8 18l-6-6l6-6l1.425 1.425l-4.6 4.6L9.4 16.6zm8 0l-1.425-1.425l4.6-4.6L14.6 7.4L16 6l6 6z"/></symbol><use href="#ai:material-symbols:code"></use> </svg> 仓库 <svg width="1em" height="1em" class="text-[1.25rem] transition-transform duration-200 dropdown-arrow ml-1" data-astro-cid-qou34bit="true" data-icon="material-symbols:keyboard-arrow-down-rounded"> <symbol id="ai:material-symbols:keyboard-arrow-down-rounded" viewBox="0 0 24 24"><path fill="currentColor" d="M11.625 14.913q-.175-.063-.325-.213l-4.6-4.6q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l3.9 3.9l3.9-3.9q.275-.275.7-.275t.7.275t.275.7t-.275.7l-4.6 4.6q-.15.15-.325.213t-.375.062t-.375-.062"/></symbol><use href="#ai:material-symbols:keyboard-arrow-down-rounded"></use> </svg> </div> </button>
|
||
<div class="dropdown-menu" data-dropdown-menu data-astro-cid-qou34bit> <div class="dropdown-content" data-astro-cid-qou34bit> <a href="https://gitee.com/starlight-apk/feature-oss" target="_blank" class="dropdown-item" aria-label="Gitee" data-astro-cid-qou34bit> <div class="flex items-center" data-astro-cid-qou34bit> <svg width="1em" height="1em" viewBox="0 0 24 24" class="text-[1rem] mr-2" data-astro-cid-qou34bit="true" data-icon="mdi:git"> <use href="#ai:mdi:git"></use> </svg> <span data-astro-cid-qou34bit>Gitee</span> </div> <svg width="1em" height="1em" class="text-[0.75rem] text-black/25 dark:text-white/25" data-astro-cid-qou34bit="true" data-icon="fa6-solid:arrow-up-right-from-square"> <symbol id="ai:fa6-solid:arrow-up-right-from-square" viewBox="0 0 512 512"><path fill="currentColor" d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32zM80 32C35.8 32 0 67.8 0 112v320c0 44.2 35.8 80 80 80h320c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32v112c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16h112c17.7 0 32-14.3 32-32s-14.3-32-32-32z"/></symbol><use href="#ai:fa6-solid:arrow-up-right-from-square"></use> </svg> </a><a href="https://github.com/starlight-apk/feature-oss" target="_blank" class="dropdown-item" aria-label="GitHub" data-astro-cid-qou34bit> <div class="flex items-center" data-astro-cid-qou34bit> <svg width="0.97em" height="1em" viewBox="0 0 496 512" class="text-[1rem] mr-2" data-astro-cid-qou34bit="true" data-icon="fa6-brands:github"> <use href="#ai:fa6-brands:github"></use> </svg> <span data-astro-cid-qou34bit>GitHub</span> </div> <svg width="1em" height="1em" viewBox="0 0 512 512" class="text-[0.75rem] text-black/25 dark:text-white/25" data-astro-cid-qou34bit="true" data-icon="fa6-solid:arrow-up-right-from-square"> <use href="#ai:fa6-solid:arrow-up-right-from-square"></use> </svg> </a> </div> </div> </div> </div> <div class="flex"> <!--<SearchPanel client:load>--> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="8ERU5" component-url="/blog/_astro/Search.C4siwRRu.js" component-export="default" renderer-url="/blog/_astro/client.svelte.9vP7DkWT.js" props="{}" ssr client="only" opts="{"name":"Search","value":"svelte"}"></astro-island> <astro-island uid="ZgLYMD" component-url="/blog/_astro/MobileTOC.DcxBJmw1.js" component-export="default" renderer-url="/blog/_astro/client.svelte.9vP7DkWT.js" props="{}" ssr client="only" opts="{"name":"MobileTOC","value":"svelte"}"></astro-island> <astro-island uid="pXLEo" component-url="/blog/_astro/TranslateButton.gpbH5i1G.js" component-export="default" renderer-url="/blog/_astro/client.svelte.9vP7DkWT.js" props="{}" ssr client="only" opts="{"name":"TranslateButton","value":"svelte"}"></astro-island> <button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch"> <svg width="1em" height="1em" class="text-[1.25rem]" data-icon="material-symbols:palette-outline"> <symbol id="ai:material-symbols:palette-outline" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22q-2.05 0-3.875-.788t-3.187-2.15t-2.15-3.187T2 12q0-2.075.813-3.9t2.2-3.175T8.25 2.788T12.2 2q2 0 3.775.688t3.113 1.9t2.125 2.875T22 11.05q0 2.875-1.75 4.413T16 17h-1.85q-.225 0-.312.125t-.088.275q0 .3.375.863t.375 1.287q0 1.25-.687 1.85T12 22m-4.425-9.425Q8 12.15 8 11.5t-.425-1.075T6.5 10t-1.075.425T5 11.5t.425 1.075T6.5 13t1.075-.425m3-4Q11 8.15 11 7.5t-.425-1.075T9.5 6t-1.075.425T8 7.5t.425 1.075T9.5 9t1.075-.425m5 0Q16 8.15 16 7.5t-.425-1.075T14.5 6t-1.075.425T13 7.5t.425 1.075T14.5 9t1.075-.425m3 4Q19 12.15 19 11.5t-.425-1.075T17.5 10t-1.075.425T16 11.5t.425 1.075T17.5 13t1.075-.425M12 20q.225 0 .363-.125t.137-.325q0-.35-.375-.825T11.75 17.3q0-1.05.725-1.675T14.25 15H16q1.65 0 2.825-.962T20 11.05q0-3.025-2.312-5.038T12.2 4Q8.8 4 6.4 6.325T4 12q0 3.325 2.338 5.663T12 20"/></symbol><use href="#ai:material-symbols:palette-outline"></use> </svg> </button> <astro-island uid="Z1LIWpd" component-url="/blog/_astro/LightDarkSwitch.DQTkCfco.js" component-export="default" renderer-url="/blog/_astro/client.svelte.9vP7DkWT.js" props="{}" ssr client="only" opts="{"name":"LightDarkSwitch","value":"svelte"}"></astro-island> <button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch"> <svg width="1em" height="1em" class="text-[1.25rem]" data-icon="material-symbols:menu-rounded"> <symbol id="ai:material-symbols:menu-rounded" viewBox="0 0 24 24"><path fill="currentColor" d="M4 18q-.425 0-.712-.288T3 17t.288-.712T4 16h16q.425 0 .713.288T21 17t-.288.713T20 18zm0-5q-.425 0-.712-.288T3 12t.288-.712T4 11h16q.425 0 .713.288T21 12t-.288.713T20 13zm0-5q-.425 0-.712-.288T3 7t.288-.712T4 6h16q.425 0 .713.288T21 7t-.288.713T20 8z"/></symbol><use href="#ai:material-symbols:menu-rounded"></use> </svg> </button> </div> <div id="nav-menu-panel" class="float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2 max-h-[80vh] overflow-y-auto" data-astro-cid-h5lvqo6o> <div class="mobile-menu-item" data-astro-cid-h5lvqo6o> <!-- 普通链接项目 -->
|
||
<a href="/blog/" class="group flex justify-between items-center py-2 pl-3 pr-1 rounded-lg gap-8
|
||
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition
|
||
" data-astro-cid-h5lvqo6o> <div class="flex items-center transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]" data-astro-cid-h5lvqo6o> <svg width="1em" height="1em" viewBox="0 0 24 24" class="text-[1.1rem] mr-2" data-astro-cid-h5lvqo6o="true" data-icon="material-symbols:home"> <use href="#ai:material-symbols:home"></use> </svg> 主页 </div> <svg width="1em" height="1em" class="transition text-[1.25rem] text-[var(--primary)]" data-astro-cid-h5lvqo6o="true" data-icon="material-symbols:chevron-right-rounded"> <symbol id="ai:material-symbols:chevron-right-rounded" viewBox="0 0 24 24"><path fill="currentColor" d="M12.6 12L8.7 8.1q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-4.6 4.6q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7z"/></symbol><use href="#ai:material-symbols:chevron-right-rounded"></use> </svg> </a> </div><div class="mobile-menu-item" data-astro-cid-h5lvqo6o> <!-- 普通链接项目 -->
|
||
<a href="/blog/archive/" class="group flex justify-between items-center py-2 pl-3 pr-1 rounded-lg gap-8
|
||
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition
|
||
" data-astro-cid-h5lvqo6o> <div class="flex items-center transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]" data-astro-cid-h5lvqo6o> <svg width="1em" height="1em" viewBox="0 0 24 24" class="text-[1.1rem] mr-2" data-astro-cid-h5lvqo6o="true" data-icon="material-symbols:archive"> <use href="#ai:material-symbols:archive"></use> </svg> 归档 </div> <svg width="1em" height="1em" viewBox="0 0 24 24" class="transition text-[1.25rem] text-[var(--primary)]" data-astro-cid-h5lvqo6o="true" data-icon="material-symbols:chevron-right-rounded"> <use href="#ai:material-symbols:chevron-right-rounded"></use> </svg> </a> </div><div class="mobile-menu-item" data-astro-cid-h5lvqo6o> <!-- 有子菜单的项目 -->
|
||
<div class="mobile-dropdown" data-mobile-dropdown data-astro-cid-h5lvqo6o> <button class="group flex justify-between items-center py-2 pl-3 pr-1 rounded-lg gap-8 w-full text-left
|
||
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition" data-mobile-dropdown-trigger aria-expanded="false" data-astro-cid-h5lvqo6o> <div class="flex items-center transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]" data-astro-cid-h5lvqo6o> <svg width="1em" height="1em" viewBox="0 0 24 24" class="text-[1.1rem] mr-2" data-astro-cid-h5lvqo6o="true" data-icon="material-symbols:code"> <use href="#ai:material-symbols:code"></use> </svg> 仓库 </div> <svg width="1em" height="1em" viewBox="0 0 24 24" class="transition text-[1.25rem] text-[var(--primary)] mobile-dropdown-arrow duration-200" data-astro-cid-h5lvqo6o="true" data-icon="material-symbols:keyboard-arrow-down-rounded"> <use href="#ai:material-symbols:keyboard-arrow-down-rounded"></use> </svg> </button> <div class="mobile-submenu" data-mobile-submenu data-astro-cid-h5lvqo6o> <a href="https://gitee.com/starlight-apk/feature-oss" class="group flex justify-between items-center py-2 pl-6 pr-1 rounded-lg gap-8
|
||
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition" target="_blank" data-astro-cid-h5lvqo6o> <div class="flex items-center transition text-black/60 dark:text-white/60 font-medium group-hover:text-[var(--primary)] group-active:text-[var(--primary)]" data-astro-cid-h5lvqo6o> <svg width="1em" height="1em" viewBox="0 0 24 24" class="text-[1.1rem] mr-2" data-astro-cid-h5lvqo6o="true" data-icon="mdi:git"> <use href="#ai:mdi:git"></use> </svg> Gitee </div> <svg width="1em" height="1em" viewBox="0 0 512 512" class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" data-astro-cid-h5lvqo6o="true" data-icon="fa6-solid:arrow-up-right-from-square"> <use href="#ai:fa6-solid:arrow-up-right-from-square"></use> </svg> </a><a href="https://github.com/starlight-apk/feature-oss" class="group flex justify-between items-center py-2 pl-6 pr-1 rounded-lg gap-8
|
||
hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)] transition" target="_blank" data-astro-cid-h5lvqo6o> <div class="flex items-center transition text-black/60 dark:text-white/60 font-medium group-hover:text-[var(--primary)] group-active:text-[var(--primary)]" data-astro-cid-h5lvqo6o> <svg width="0.97em" height="1em" viewBox="0 0 496 512" class="text-[1.1rem] mr-2" data-astro-cid-h5lvqo6o="true" data-icon="fa6-brands:github"> <use href="#ai:fa6-brands:github"></use> </svg> GitHub </div> <svg width="1em" height="1em" viewBox="0 0 512 512" class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" data-astro-cid-h5lvqo6o="true" data-icon="fa6-solid:arrow-up-right-from-square"> <use href="#ai:fa6-solid:arrow-up-right-from-square"></use> </svg> </a> </div> </div> </div> </div> <script type="module">document.addEventListener("DOMContentLoaded",function(){const r=document.querySelectorAll("[data-mobile-dropdown]");r.forEach(e=>{const t=e.querySelector("[data-mobile-dropdown-trigger]"),d=e.querySelector("[data-mobile-submenu]");!t||!d||t.addEventListener("click",function(o){o.preventDefault();const u=e.getAttribute("data-expanded")==="true";r.forEach(a=>{if(a!==e){a.setAttribute("data-expanded","false");const i=a.querySelector("[data-mobile-dropdown-trigger]");i&&i.setAttribute("aria-expanded","false")}});const n=!u;e.setAttribute("data-expanded",n.toString()),t.setAttribute("aria-expanded",n.toString())})})});</script> <astro-island uid="i6lC1" component-url="/blog/_astro/DisplaySettings.BkjQOgCB.js" component-export="default" renderer-url="/blog/_astro/client.svelte.9vP7DkWT.js" props="{}" ssr client="only" opts="{"name":"DisplaySettings","value":"svelte"}"></astro-island> </div> </div> <script type="module">function c(){localStorage.theme==="dark"?(document.documentElement.classList.remove("dark"),localStorage.theme="light"):(document.documentElement.classList.add("dark"),localStorage.theme="dark")}function d(){let e=document.getElementById("scheme-switch");e&&(e.onclick=function(){c()});let l=document.getElementById("display-settings-switch");l&&(l.onclick=function(){let t=document.getElementById("display-setting");t&&t.classList.toggle("float-panel-closed")});let n=document.getElementById("nav-menu-switch");n&&(n.onclick=function(){let t=document.getElementById("nav-menu-panel");t&&t.classList.toggle("float-panel-closed")})}d();function o(){const e=document.getElementById("navbar");if(!e||e.getAttribute("data-transparent-mode")!=="semifull")return;if(!(e.getAttribute("data-is-home")==="true")){window.semifullScrollHandler&&(window.removeEventListener("scroll",window.semifullScrollHandler),window.semifullScrollHandler=null),e.classList.add("scrolled");return}e.classList.remove("scrolled");let t=!1;function i(){(window.pageYOffset||document.documentElement.scrollTop)>50?e.classList.add("scrolled"):e.classList.remove("scrolled"),t=!1}function s(){t||(requestAnimationFrame(i),t=!0)}window.semifullScrollHandler&&window.removeEventListener("scroll",window.semifullScrollHandler),window.semifullScrollHandler=s,window.addEventListener("scroll",s,{passive:!0}),i()}window.initSemifullScrollDetection=o;document.readyState==="loading"?document.addEventListener("DOMContentLoaded",o):o();</script> <script>(function(){const scriptUrl = "/blog/pagefind/pagefind.js";
|
||
|
||
async function loadPagefind() {
|
||
try {
|
||
const response = await fetch(scriptUrl, { method: 'HEAD' });
|
||
if (!response.ok) {
|
||
throw new Error(`Pagefind script not found: ${response.status}`);
|
||
}
|
||
|
||
const pagefind = await import(scriptUrl);
|
||
|
||
await pagefind.options({
|
||
excerptLength: 20
|
||
});
|
||
|
||
window.pagefind = pagefind;
|
||
|
||
document.dispatchEvent(new CustomEvent('pagefindready'));
|
||
console.log('Pagefind loaded and initialized successfully, event dispatched.');
|
||
} catch (error) {
|
||
console.error('Failed to load Pagefind:', error);
|
||
window.pagefind = {
|
||
search: () => Promise.resolve({ results: [] }),
|
||
options: () => Promise.resolve(),
|
||
};
|
||
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
|
||
console.log('Pagefind load error, event dispatched.');
|
||
}
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', loadPagefind);
|
||
} else {
|
||
loadPagefind();
|
||
}
|
||
})();</script> </div> </div> <div id="banner-wrapper" class="absolute z-10 w-full transition duration-700 overflow-hidden mobile-hide-banner" style="top: -30vh" data-astro-cid-haiuh7kc> <!-- Single image mode -->
|
||
<div class="relative h-full w-full" data-astro-cid-haiuh7kc> <!-- Mobile: use mobile-specific image with same logic as desktop --> <div class="block lg:hidden object-cover h-full w-full transition duration-700 opacity-100 overflow-hidden relative"> <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div> <img src="/blog/assets/placeholder.jpg" alt="Mobile banner image of the blog" class="w-full h-full object-cover" style="object-position: center"> </div> <!-- Desktop: use desktop-specific image --> <div id="banner" class="hidden lg:block object-cover h-full transition duration-700 opacity-100 overflow-hidden relative"> <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div> <img src="/blog/assets/placeholder.jpg" alt="Desktop banner image of the blog" class="w-full h-full object-cover" style="object-position: center"> </div> </div> <!-- Home page text overlay --> <div class="banner-text-overlay absolute inset-0 z-20 flex items-center justify-center hidden" data-astro-cid-haiuh7kc> <div class="w-4/5 lg:w-3/4 text-center mb-0" data-astro-cid-haiuh7kc> <div class="flex flex-col" data-astro-cid-haiuh7kc> <h1 class="banner-title text-6xl lg:text-8xl font-bold text-white drop-shadow-lg mb-2 lg:mb-4" data-astro-cid-haiuh7kc> FutureOSS </h1> <h2 class="banner-subtitle text-xl lg:text-3xl text-white/90 drop-shadow-md" data-astro-cid-haiuh7kc> <span class="typewriter " data-text="["一切皆为插件的开发者工具运行时框架","插件热插拔 · 依赖自动解析 · 熔断降级 · 事件驱动"]" data-speed="100" data-delete-speed="50" data-pause-time="2000" data-astro-cid-4iv2rnoh></span> <script type="module">class n{element;texts;currentTextIndex=0;speed;deleteSpeed;pauseTime;currentIndex=0;isDeleting=!1;timeoutId=null;constructor(t){this.element=t;const e=t.dataset.text||"";try{const i=JSON.parse(e);this.texts=Array.isArray(i)?i:[e]}catch{this.texts=[e]}this.speed=parseInt(t.dataset.speed||"100"),this.deleteSpeed=parseInt(t.dataset.deleteSpeed||"50"),this.pauseTime=parseInt(t.dataset.pauseTime||"2000"),this.texts.length>1&&!this.isTypewriterEnabled()?this.showRandomText():this.start()}isTypewriterEnabled(){return this.element.dataset.speed!==void 0||this.element.dataset.deleteSpeed!==void 0||this.element.dataset.pauseTime!==void 0}showRandomText(){const t=Math.floor(Math.random()*this.texts.length);this.element.textContent=this.texts[t]}start(){this.texts.length!==0&&this.type()}getCurrentText(){return this.texts[this.currentTextIndex]||""}type(){const t=this.getCurrentText();this.isDeleting?this.currentIndex>0?(this.currentIndex--,this.element.textContent=t.substring(0,this.currentIndex),this.timeoutId=window.setTimeout(()=>this.type(),this.deleteSpeed)):(this.isDeleting=!1,this.currentTextIndex=(this.currentTextIndex+1)%this.texts.length,this.timeoutId=window.setTimeout(()=>this.type(),this.speed)):this.currentIndex<t.length?(this.currentIndex++,this.element.textContent=t.substring(0,this.currentIndex),this.timeoutId=window.setTimeout(()=>this.type(),this.speed)):this.texts.length>1&&(this.isDeleting=!0,this.timeoutId=window.setTimeout(()=>this.type(),this.pauseTime))}destroy(){this.timeoutId&&clearTimeout(this.timeoutId)}}document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll(".typewriter").forEach(t=>{new n(t)})});document.addEventListener("swup:contentReplaced",()=>{document.querySelectorAll(".typewriter").forEach(t=>{new n(t)})});</script> </h2> </div> </div> </div> <!-- Water waves effect --> <div class="waves absolute -bottom-[1px] h-[10vh] max-h-[9.375rem] min-h-[3.125rem] w-full md:h-[15vh]" id="header-waves" style="transform: translateZ(0); will-change: fill;" data-astro-cid-haiuh7kc> <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 20 150 32" preserveAspectRatio="none" shape-rendering="auto" style="transform: translateZ(0); backface-visibility: hidden;" data-astro-cid-haiuh7kc> <defs data-astro-cid-haiuh7kc> <path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v48h-352z" data-astro-cid-haiuh7kc></path> </defs> <g class="parallax" style="transform: translateZ(0);" data-astro-cid-haiuh7kc> <use xlink:href="#gentle-wave" x="48" y="0" class="opacity-25 fill-[var(--page-bg)]" style="animation-delay: -2s; animation-duration: 7s; will-change: transform;" data-astro-cid-haiuh7kc></use> <use xlink:href="#gentle-wave" x="48" y="3" class="opacity-50 fill-[var(--page-bg)]" style="animation-delay: -3s; animation-duration: 10s; will-change: transform;" data-astro-cid-haiuh7kc></use> <use xlink:href="#gentle-wave" x="48" y="5" class="opacity-75 fill-[var(--page-bg)]" style="animation-delay: -4s; animation-duration: 13s; will-change: transform;" data-astro-cid-haiuh7kc></use> <use xlink:href="#gentle-wave" x="48" y="7" class="fill-[var(--page-bg)]" style="animation-delay: -5s; animation-duration: 20s; will-change: transform;" data-astro-cid-haiuh7kc></use> </g> </svg> </div> </div> <div class="absolute w-full z-30 pointer-events-none mobile-main-no-banner " style="top: calc(35vh - 3.5rem)" data-astro-cid-haiuh7kc> <!-- The pointer-events-none here prevent blocking the click event of the TOC --> <div class="relative max-w-[var(--page-width)] mx-auto pointer-events-auto" data-astro-cid-haiuh7kc> <div id="main-grid" class="transition duration-700 w-full left-0 right-0 grid grid-cols-1 md:grid-cols-[17.5rem_1fr] lg:grid-cols-[17.5rem_1fr] grid-rows-[auto_1fr_auto] lg:grid-rows-[auto] mx-auto gap-4 px-0 md:px-4 " data-astro-cid-haiuh7kc> <!-- Banner image credit --> <div id="sidebar" class="mb-4 row-start-2 row-end-3 col-span-2 onload-animation block md:block md:row-start-1 md:row-end-2 md:max-w-[17.5rem] md:col-start-1 md:col-end-2 lg:block lg:row-start-1 lg:row-end-2 lg:max-w-[17.5rem] lg:col-start-1 lg:col-end-2 w-full" data-astro-cid-gfmxq3pg> <!-- 顶部固定组件区域 --> <div class="flex flex-col w-full gap-4 mb-4" data-astro-cid-gfmxq3pg> <div class="card-base p-3"> <a aria-label="Go to About Page" href="/blog/about/" class="group block relative mx-auto mt-1 lg:mx-0 lg:mt-0 mb-3
|
||
max-w-[12rem] lg:max-w-none overflow-hidden rounded-xl active:scale-95"> <div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
|
||
w-full h-full z-50 flex items-center justify-center"> <svg width="1.13em" height="1em" class="transition opacity-0 scale-90 group-hover:scale-100 group-hover:opacity-100 text-white text-5xl" data-icon="fa6-regular:address-card"> <symbol id="ai:fa6-regular:address-card" viewBox="0 0 576 512"><path fill="currentColor" d="M512 80c8.8 0 16 7.2 16 16v320c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V96c0-8.8 7.2-16 16-16zM64 32C28.7 32 0 60.7 0 96v320c0 35.3 28.7 64 64 64h448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm144 224a64 64 0 1 0 0-128a64 64 0 1 0 0 128m-32 32c-44.2 0-80 35.8-80 80c0 8.8 7.2 16 16 16h192c8.8 0 16-7.2 16-16c0-44.2-35.8-80-80-80zm200-144c-13.3 0-24 10.7-24 24s10.7 24 24 24h80c13.3 0 24-10.7 24-24s-10.7-24-24-24zm0 96c-13.3 0-24 10.7-24 24s10.7 24 24 24h80c13.3 0 24-10.7 24-24s-10.7-24-24-24z"/></symbol><use href="#ai:fa6-regular:address-card"></use> </svg> </div> <div class="mx-auto lg:w-full h-full lg:mt-0 overflow-hidden relative"> <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div> <img src="/blog/_astro/avatar.DslDo0tY_Z2tanD.webp" alt="Profile Image of the Author" style="object-position: center" loading="lazy" decoding="async" fetchpriority="auto" width="640" height="640" class="w-full h-full object-cover"> </div> </a> <div class="px-2"> <div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">FutureOSS</div> <div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div> <div class="text-center text-neutral-400 mb-2.5 transition">一切皆为插件的开发者工具运行时框架</div> <div class="flex gap-2 justify-center mb-1"> <a rel="me" aria-label="Gitee" href="https://gitee.com/starlight-apk/feature-oss" target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90"> <svg width="1em" height="1em" class="text-[1.5rem]" data-icon="mdi:git"> <symbol id="ai:mdi:git" viewBox="0 0 24 24"><path fill="currentColor" d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2a2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82"/></symbol><use href="#ai:mdi:git"></use> </svg> </a><a rel="me" aria-label="GitHub" href="https://github.com/starlight-apk/feature-oss" target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90"> <svg width="0.97em" height="1em" class="text-[1.5rem]" data-icon="fa6-brands:github"> <symbol id="ai:fa6-brands:github" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2"/></symbol><use href="#ai:fa6-brands:github"></use> </svg> </a> </div> </div> </div><!-- 组件显示现在由sidebarLayoutConfig统一控制,无需检查config.enable --><widget-layout data-id="announcement" data-is-collapsed="undefined" class="pb-4 card-base onload-animation" style="animation-delay: 50ms; " data-astro-cid-ucso7hve="true"> <div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
|
||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||
before:absolute before:left-[-16px] before:top-[5.5px]" data-astro-cid-ucso7hve style="">欢迎来到 FutureOSS</div> <div id="announcement" class="collapse-wrapper px-4 overflow-hidden" data-astro-cid-ucso7hve style=""> <div> <!-- 公告栏内容 --> <div class="text-neutral-600 dark:text-neutral-300 leading-relaxed mb-3"> 一切皆为插件的开发者工具运行时框架,支持插件热插拔、依赖自动解析、熔断降级、事件驱动等企业级稳定性机制。 </div> <!-- 可选链接和关闭按钮 --> <div class="flex items-center justify-between gap-3"> <div> <a href="https://gitee.com/starlight-apk/feature-oss/wikis/Home" target="_blank" rel="noopener noreferrer" class="btn-regular rounded-lg px-3 py-1.5 text-sm font-medium active:scale-95 transition-transform"> 快速开始 </a> </div> <button class="btn-regular rounded-lg h-8 w-8 text-sm hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors" onclick="closeAnnouncement()" aria-label="关闭"> <svg width="0.75em" height="1em" class="text-sm" data-icon="fa6-solid:xmark"> <symbol id="ai:fa6-solid:xmark" viewBox="0 0 384 512"><path fill="currentColor" d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7L86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256L41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256z"/></symbol><use href="#ai:fa6-solid:xmark"></use> </svg> </button> </div> </div> </div> </widget-layout> <script type="module">class d extends HTMLElement{constructor(){if(super(),this.dataset.isCollapsed!=="true")return;const e=this.dataset.id,t=this.querySelector(".expand-btn"),s=this.querySelector(`#${e}`);t.addEventListener("click",()=>{s.classList.remove("collapsed"),t.classList.add("hidden")})}}customElements.get("widget-layout")||customElements.define("widget-layout",d);</script> <script type="module">function n(){const e=document.querySelector('widget-layout[data-id="announcement"]');e&&(e.style.display="none",localStorage.setItem("announcementClosed","true"))}document.addEventListener("DOMContentLoaded",function(){const e=document.querySelector('widget-layout[data-id="announcement"]');e&&localStorage.getItem("announcementClosed")==="true"&&(e.style.display="none")});window.closeAnnouncement=n;</script> </div> <!-- 粘性组件区域 --> <div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4" data-astro-cid-gfmxq3pg> <widget-layout data-id="categories" data-is-collapsed="false" class="pb-4 card-base onload-animation" style="animation-delay: 150ms; --collapsedHeight: 7.5rem;" data-astro-cid-ucso7hve="true"> <div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
|
||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||
before:absolute before:left-[-16px] before:top-[5.5px]" data-astro-cid-ucso7hve style="--collapsedHeight: 7.5rem;">分类</div> <div id="categories" class="collapse-wrapper px-4 overflow-hidden" data-astro-cid-ucso7hve style="--collapsedHeight: 7.5rem;"> <a href="/blog/archive/?category=Examples" aria-label="View all posts in the Examples category"> <button class="
|
||
w-full
|
||
h-10
|
||
rounded-lg
|
||
bg-none
|
||
hover:bg-[var(--btn-plain-bg-hover)]
|
||
active:bg-[var(--btn-plain-bg-active)]
|
||
transition-all
|
||
pl-2
|
||
hover:pl-3
|
||
|
||
text-neutral-700
|
||
hover:text-[var(--primary)]
|
||
dark:text-neutral-300
|
||
dark:hover:text-[var(--primary)]
|
||
"> <div class="flex items-center justify-between relative mr-2"> <div class="overflow-hidden text-left whitespace-nowrap overflow-ellipsis "> Examples </div> <div class="transition px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
|
||
text-[var(--btn-content)] dark:text-[var(--deep-text)]
|
||
bg-[oklch(0.95_0.025_var(--hue))] dark:bg-[var(--primary)]
|
||
flex items-center justify-center"> 4 </div> </div> </button> </a> </div> </widget-layout> <widget-layout data-id="tags" data-is-collapsed="false" class="pb-4 card-base onload-animation" style="animation-delay: 200ms; --collapsedHeight: 7.5rem;" data-astro-cid-ucso7hve="true"> <div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
|
||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||
before:absolute before:left-[-16px] before:top-[5.5px]" data-astro-cid-ucso7hve style="--collapsedHeight: 7.5rem;">标签</div> <div id="tags" class="collapse-wrapper px-4 overflow-hidden" data-astro-cid-ucso7hve style="--collapsedHeight: 7.5rem;"> <div class="flex gap-2 flex-wrap"> <a href="/blog/archive/?tag=Blogging" aria-label="View all posts with the Blogging tag" class="btn-regular h-8 text-sm px-3 rounded-lg"> Blogging </a><a href="/blog/archive/?tag=Example" aria-label="View all posts with the Example tag" class="btn-regular h-8 text-sm px-3 rounded-lg"> Example </a><a href="/blog/archive/?tag=Markdown" aria-label="View all posts with the Markdown tag" class="btn-regular h-8 text-sm px-3 rounded-lg"> Markdown </a><a href="/blog/archive/?tag=Mermaid" aria-label="View all posts with the Mermaid tag" class="btn-regular h-8 text-sm px-3 rounded-lg"> Mermaid </a><a href="/blog/archive/?tag=Video" aria-label="View all posts with the Video tag" class="btn-regular h-8 text-sm px-3 rounded-lg"> Video </a> </div> </div> </widget-layout> </div> </div> <!-- 响应式样式和JavaScript --> <script type="module" src="/blog/_astro/SideBar.astro_astro_type_script_index_0_lang.Fy0FqEdY.js"></script> <main id="swup-container" class="transition-swup-fade overflow-hidden w-full col-span-2 md:col-start-2 md:col-end-3 lg:col-start-2 lg:col-end-3" data-astro-cid-haiuh7kc> <div id="content-wrapper" class="onload-animation" data-astro-cid-haiuh7kc> <!-- the overflow-hidden here prevent long text break the layout--> <!-- make id different from windows.swup global property --> <!-- Iconify图标库加载器 --><script>(function(){const preloadIcons = ["logos:javascript","logos:typescript-icon","logos:react","logos:vue","logos:astro-icon","logos:tailwindcss-icon","logos:nodejs-icon","logos:python","simple-icons:express","logos:postgresql","logos:mongodb-icon","simple-icons:firebase","logos:git-icon","logos:visual-studio-code","logos:docker-icon","logos:figma"];
|
||
const timeout = 10000;
|
||
const retryCount = 3;
|
||
|
||
// 全局图标加载逻辑
|
||
(function() {
|
||
'use strict';
|
||
|
||
// 避免重复加载
|
||
if (window.__iconifyLoaderInitialized) {
|
||
return;
|
||
}
|
||
window.__iconifyLoaderInitialized = true;
|
||
|
||
// 图标加载器类
|
||
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 = timeout, retryCount: maxRetries = retryCount } = 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(timeout, retryCount) {
|
||
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
||
try {
|
||
await this.loadScript(timeout);
|
||
return;
|
||
} catch (error) {
|
||
console.warn(`Iconify load attempt ${attempt} failed:`, error);
|
||
|
||
if (attempt === retryCount) {
|
||
throw new Error(`Failed to load Iconify after ${retryCount} attempts`);
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
}
|
||
}
|
||
|
||
loadScript(timeout) {
|
||
return new Promise((resolve, reject) => {
|
||
// 检查是否已经存在
|
||
if (this.isIconifyReady()) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const existingScript = document.querySelector('script[src*="iconify-icon"]');
|
||
if (existingScript) {
|
||
this.waitForIconifyReady().then(resolve).catch(reject);
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js';
|
||
script.async = true;
|
||
script.crossOrigin = 'anonymous';
|
||
|
||
const timeoutId = setTimeout(() => {
|
||
script.remove();
|
||
reject(new Error('Script load timeout'));
|
||
}, timeout);
|
||
|
||
script.onload = () => {
|
||
clearTimeout(timeoutId);
|
||
this.waitForIconifyReady().then(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() {
|
||
if (this.preloadQueue.size === 0) return;
|
||
|
||
const iconsToLoad = Array.from(this.preloadQueue);
|
||
this.preloadQueue.clear();
|
||
|
||
await this.preloadIcons(iconsToLoad);
|
||
}
|
||
|
||
async preloadIcons(icons) {
|
||
if (!this.isLoaded || icons.length === 0) return;
|
||
|
||
return new Promise((resolve) => {
|
||
let loadedCount = 0;
|
||
const totalIcons = icons.length;
|
||
const tempElements = [];
|
||
|
||
const cleanup = () => {
|
||
tempElements.forEach(el => {
|
||
if (el.parentNode) {
|
||
el.parentNode.removeChild(el);
|
||
}
|
||
});
|
||
};
|
||
|
||
const checkComplete = () => {
|
||
loadedCount++;
|
||
if (loadedCount >= totalIcons) {
|
||
cleanup();
|
||
resolve();
|
||
}
|
||
};
|
||
|
||
icons.forEach(icon => {
|
||
const tempIcon = document.createElement('iconify-icon');
|
||
tempIcon.setAttribute('icon', icon);
|
||
tempIcon.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;';
|
||
|
||
tempIcon.addEventListener('load', checkComplete);
|
||
tempIcon.addEventListener('error', checkComplete);
|
||
|
||
tempElements.push(tempIcon);
|
||
document.body.appendChild(tempIcon);
|
||
});
|
||
|
||
// 设置超时清理
|
||
setTimeout(() => {
|
||
cleanup();
|
||
resolve();
|
||
}, 3000);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 创建全局实例
|
||
window.__iconifyLoader = new IconifyLoader();
|
||
|
||
// 立即开始加载
|
||
window.__iconifyLoader.load().catch(error => {
|
||
console.error('Failed to initialize Iconify:', error);
|
||
});
|
||
|
||
// 如果有预加载图标,添加到队列
|
||
if (preloadIcons && 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> <!-- 为不支持JavaScript的情况提供备用方案 --><noscript><style>
|
||
iconify-icon {
|
||
display: none;
|
||
}
|
||
.icon-fallback {
|
||
display: inline-block;
|
||
}
|
||
</style></noscript> <div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32" data-astro-cid-xahix5fp> <div class="card-base z-10 px-9 py-6 relative w-full" data-astro-cid-xahix5fp> <!-- 页面标题 --> <div class="flex flex-col items-start justify-center mb-8" data-astro-cid-xahix5fp> <h1 class="text-4xl font-bold text-black/90 dark:text-white/90 mb-2" data-astro-cid-xahix5fp> 技能展示 </h1> <p class="text-lg text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 我的技术技能和专业知识 </p> </div> <!-- 统计信息 --> <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8" data-astro-cid-xahix5fp> <div class="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-lg p-4" data-astro-cid-xahix5fp> <div class="text-2xl font-bold text-blue-600 dark:text-blue-400" data-astro-cid-xahix5fp>16</div> <div class="text-sm text-blue-600/70 dark:text-blue-400/70" data-astro-cid-xahix5fp>总技能数</div> </div> <div class="bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 rounded-lg p-4" data-astro-cid-xahix5fp> <div class="text-2xl font-bold text-red-600 dark:text-red-400" data-astro-cid-xahix5fp>1</div> <div class="text-sm text-red-600/70 dark:text-red-400/70" data-astro-cid-xahix5fp>专家级</div> </div> <div class="bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/20 rounded-lg p-4" data-astro-cid-xahix5fp> <div class="text-2xl font-bold text-orange-600 dark:text-orange-400" data-astro-cid-xahix5fp>6</div> <div class="text-sm text-orange-600/70 dark:text-orange-400/70" data-astro-cid-xahix5fp>高级</div> </div> <div class="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-lg p-4" data-astro-cid-xahix5fp> <div class="text-2xl font-bold text-purple-600 dark:text-purple-400" data-astro-cid-xahix5fp> 32 </div> <div class="text-sm text-purple-600/70 dark:text-purple-400/70" data-astro-cid-xahix5fp>经验</div> </div> </div> <!-- 专业技能 --> <div class="mb-8" data-astro-cid-xahix5fp> <h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-4" data-astro-cid-xahix5fp> 专业技能 </h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-6" data-astro-cid-xahix5fp> <div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #F7DF1E20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #F7DF1E;" data-icon-container="icon-rp47f6xuo" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> J </span> <!-- 实际图标 --> <iconify-icon icon="logos:javascript" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-rp47f6xuo";
|
||
const icon = "logos:javascript";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> JavaScript </h3> <span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 现代JavaScript开发,包括ES6+语法、异步编程、模块化开发等。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 3年 6个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 80%; background-color: #F7DF1E" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #3178C620" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #3178C6;" data-icon-container="icon-gl6tpmcyc" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> T </span> <!-- 实际图标 --> <iconify-icon icon="logos:typescript-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-gl6tpmcyc";
|
||
const icon = "logos:typescript-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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> TypeScript </h3> <span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 类型安全的JavaScript超集,提升代码质量和开发效率。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 8个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 80%; background-color: #3178C6" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #61DAFB20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #61DAFB;" data-icon-container="icon-ht207m69q" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> R </span> <!-- 实际图标 --> <iconify-icon icon="logos:react" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-ht207m69q";
|
||
const icon = "logos:react";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> React </h3> <span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 构建用户界面的JavaScript库,包括Hooks、Context、状态管理等。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 10个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 80%; background-color: #61DAFB" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #FF5D0120" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #FF5D01;" data-icon-container="icon-89s6x02f8" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> A </span> <!-- 实际图标 --> <iconify-icon icon="logos:astro-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-89s6x02f8";
|
||
const icon = "logos:astro-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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> Astro </h3> <span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 现代静态站点生成器,支持多框架集成和优秀的性能。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 2个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 80%; background-color: #FF5D01" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #06B6D420" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #06B6D4;" data-icon-container="icon-qg474oe3c" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> T </span> <!-- 实际图标 --> <iconify-icon icon="logos:tailwindcss-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-qg474oe3c";
|
||
const icon = "logos:tailwindcss-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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> Tailwind CSS </h3> <span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 实用优先的CSS框架,快速构建现代化用户界面。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 80%; background-color: #06B6D4" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #F0503220" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #F05032;" data-icon-container="icon-7q67jt61m" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> G </span> <!-- 实际图标 --> <iconify-icon icon="logos:git-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-7q67jt61m";
|
||
const icon = "logos:git-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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> Git </h3> <span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 分布式版本控制系统,代码管理和团队协作必备工具。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 3年 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 80%; background-color: #F05032" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-6 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-4" data-astro-cid-xahix5fp> <div class="w-12 h-12 rounded-lg flex items-center justify-center" style="background-color: #007ACC20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-2xl" style="color: #007ACC;" data-icon-container="icon-k8whntapy" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> V </span> <!-- 实际图标 --> <iconify-icon icon="logos:visual-studio-code" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-k8whntapy";
|
||
const icon = "logos:visual-studio-code";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-2" data-astro-cid-xahix5fp> <h3 class="text-xl font-semibold text-black/90 dark:text-white/90" data-astro-cid-xahix5fp> VS Code </h3> <span class="px-2 py-1 text-xs rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" data-astro-cid-xahix5fp> 专家级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-3 text-sm" data-astro-cid-xahix5fp> 轻量级但功能强大的代码编辑器,丰富的插件生态。 </p> <div class="mb-3" data-astro-cid-xahix5fp> <div class="flex justify-between text-sm mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 3年 6个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-300" style="width: 100%; background-color: #007ACC" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div> </div> </div> <!-- 按分类展示技能 --> <div class="space-y-8" data-astro-cid-xahix5fp> <div data-astro-cid-xahix5fp> <h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-4" data-astro-cid-xahix5fp> 前端开发 <span class="text-lg font-normal text-black/60 dark:text-white/60 ml-2" data-astro-cid-xahix5fp>
|
||
(6)
|
||
</span> </h2> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-astro-cid-xahix5fp> <div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #F7DF1E20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #F7DF1E;" data-icon-container="icon-z5vgm6mvq" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> J </span> <!-- 实际图标 --> <iconify-icon icon="logos:javascript" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-z5vgm6mvq";
|
||
const icon = "logos:javascript";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> JavaScript </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 现代JavaScript开发,包括ES6+语法、异步编程、模块化开发等。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 3年 6个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 80%; background-color: #F7DF1E" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 3 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #3178C620" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #3178C6;" data-icon-container="icon-9f8ibbe8h" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> T </span> <!-- 实际图标 --> <iconify-icon icon="logos:typescript-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-9f8ibbe8h";
|
||
const icon = "logos:typescript-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> TypeScript </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 类型安全的JavaScript超集,提升代码质量和开发效率。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 8个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 80%; background-color: #3178C6" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 3 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #61DAFB20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #61DAFB;" data-icon-container="icon-813e9zm7j" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> R </span> <!-- 实际图标 --> <iconify-icon icon="logos:react" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-813e9zm7j";
|
||
const icon = "logos:react";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> React </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 构建用户界面的JavaScript库,包括Hooks、Context、状态管理等。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 10个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 80%; background-color: #61DAFB" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 2 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #4FC08D20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #4FC08D;" data-icon-container="icon-akhqey6xd" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> V </span> <!-- 实际图标 --> <iconify-icon icon="logos:vue" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-akhqey6xd";
|
||
const icon = "logos:vue";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Vue.js </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 渐进式JavaScript框架,易学易用,适合快速开发。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 8个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #4FC08D" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 1 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #FF5D0120" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #FF5D01;" data-icon-container="icon-oky9j58ib" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> A </span> <!-- 实际图标 --> <iconify-icon icon="logos:astro-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-oky9j58ib";
|
||
const icon = "logos:astro-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Astro </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 现代静态站点生成器,支持多框架集成和优秀的性能。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 2个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 80%; background-color: #FF5D01" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 1 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #06B6D420" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #06B6D4;" data-icon-container="icon-lq4q5vit8" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> T </span> <!-- 实际图标 --> <iconify-icon icon="logos:tailwindcss-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-lq4q5vit8";
|
||
const icon = "logos:tailwindcss-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Tailwind CSS </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 实用优先的CSS框架,快速构建现代化用户界面。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 80%; background-color: #06B6D4" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 2 </div> </div> </div> </div> </div> </div><div data-astro-cid-xahix5fp> <h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-4" data-astro-cid-xahix5fp> 后端开发 <span class="text-lg font-normal text-black/60 dark:text-white/60 ml-2" data-astro-cid-xahix5fp>
|
||
(3)
|
||
</span> </h2> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-astro-cid-xahix5fp> <div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #33993320" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #339933;" data-icon-container="icon-g9ty6kqdb" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> N </span> <!-- 实际图标 --> <iconify-icon icon="logos:nodejs-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-g9ty6kqdb";
|
||
const icon = "logos:nodejs-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Node.js </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 基于Chrome V8引擎的JavaScript运行时,用于服务端开发。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 2年 3个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #339933" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 2 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #3776AB20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #3776AB;" data-icon-container="icon-y45rnouuh" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> P </span> <!-- 实际图标 --> <iconify-icon icon="logos:python" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-y45rnouuh";
|
||
const icon = "logos:python";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Python </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 通用编程语言,适用于Web开发、数据分析、机器学习等。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 10个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #3776AB" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #00000020" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #000000;" data-icon-container="icon-ckrke0t9j" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> E </span> <!-- 实际图标 --> <iconify-icon icon="simple-icons:express" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-ckrke0t9j";
|
||
const icon = "simple-icons:express";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Express.js </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 快速、极简的Node.js Web应用框架。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 8个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #000000" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 1 </div> </div> </div> </div> </div> </div><div data-astro-cid-xahix5fp> <h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-4" data-astro-cid-xahix5fp> 数据库 <span class="text-lg font-normal text-black/60 dark:text-white/60 ml-2" data-astro-cid-xahix5fp>
|
||
(3)
|
||
</span> </h2> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-astro-cid-xahix5fp> <div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #33679120" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #336791;" data-icon-container="icon-vjrln3gpg" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> P </span> <!-- 实际图标 --> <iconify-icon icon="logos:postgresql" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-vjrln3gpg";
|
||
const icon = "logos:postgresql";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> PostgreSQL </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 强大的开源关系型数据库管理系统。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 5个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #336791" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 1 </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #47A24820" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #47A248;" data-icon-container="icon-4v2uftpyj" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> M </span> <!-- 实际图标 --> <iconify-icon icon="logos:mongodb-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-4v2uftpyj";
|
||
const icon = "logos:mongodb-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> MongoDB </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 面向文档的NoSQL数据库,灵活的数据模型。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 2个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #47A248" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #FFCA2820" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #FFCA28;" data-icon-container="icon-9jofnz15q" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> F </span> <!-- 实际图标 --> <iconify-icon icon="simple-icons:firebase" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-9jofnz15q";
|
||
const icon = "simple-icons:firebase";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Firebase </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> Google的移动和Web应用开发平台,提供实时数据库和认证服务。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 0年 10个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #FFCA28" data-astro-cid-xahix5fp></div> </div> </div> <div class="text-xs text-black/60 dark:text-white/60" data-astro-cid-xahix5fp> 相关项目: 1 </div> </div> </div> </div> </div> </div><div data-astro-cid-xahix5fp> <h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-4" data-astro-cid-xahix5fp> 开发工具 <span class="text-lg font-normal text-black/60 dark:text-white/60 ml-2" data-astro-cid-xahix5fp>
|
||
(4)
|
||
</span> </h2> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-astro-cid-xahix5fp> <div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #F0503220" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #F05032;" data-icon-container="icon-ljy9mhppv" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> G </span> <!-- 实际图标 --> <iconify-icon icon="logos:git-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-ljy9mhppv";
|
||
const icon = "logos:git-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Git </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" data-astro-cid-xahix5fp> 高级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 分布式版本控制系统,代码管理和团队协作必备工具。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 3年 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 80%; background-color: #F05032" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #007ACC20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #007ACC;" data-icon-container="icon-j9yr1y8ny" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> V </span> <!-- 实际图标 --> <iconify-icon icon="logos:visual-studio-code" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-j9yr1y8ny";
|
||
const icon = "logos:visual-studio-code";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> VS Code </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" data-astro-cid-xahix5fp> 专家级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 轻量级但功能强大的代码编辑器,丰富的插件生态。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 3年 6个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 100%; background-color: #007ACC" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #2496ED20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #2496ED;" data-icon-container="icon-z60urkko4" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> D </span> <!-- 实际图标 --> <iconify-icon icon="logos:docker-icon" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-z60urkko4";
|
||
const icon = "logos:docker-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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Docker </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 容器化平台,简化应用部署和环境管理。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #2496ED" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div><div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 p-4 hover:shadow-lg transition-shadow duration-300" data-astro-cid-xahix5fp> <div class="flex items-start gap-3" data-astro-cid-xahix5fp> <div class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0" style="background-color: #F24E1E20" data-astro-cid-xahix5fp> <span class="inline-flex items-center justify-center text-base text-lg" style="color: #F24E1E;" data-icon-container="icon-ykp1wuwjq" data-astro-cid-qpfihmil> <!-- 加载状态指示器 --> <span class="icon-loading animate-pulse opacity-50" data-loading-indicator data-astro-cid-qpfihmil> F </span> <!-- 实际图标 --> <iconify-icon icon="logos:figma" class="icon-content opacity-0 transition-opacity duration-200" data-icon-element="true" loading="eager" data-astro-cid-qpfihmil="true"></iconify-icon> </span> <script>(function(){const iconId = "icon-ykp1wuwjq";
|
||
const icon = "logos:figma";
|
||
|
||
// 图标加载和显示逻辑
|
||
(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> </div> <div class="flex-1 min-w-0" data-astro-cid-xahix5fp> <div class="flex items-center justify-between mb-1" data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/90 dark:text-white/90 truncate" data-astro-cid-xahix5fp> Figma </h3> <span class="px-2 py-1 text-xs rounded-full shrink-0 ml-2 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" data-astro-cid-xahix5fp> 中级 </span> </div> <p class="text-black/60 dark:text-white/60 mb-2 text-sm line-clamp-2" data-astro-cid-xahix5fp> 协作式界面设计工具,用于UI/UX设计和原型制作。 </p> <div class="mb-2" data-astro-cid-xahix5fp> <div class="flex justify-between text-xs mb-1" data-astro-cid-xahix5fp> <span class="text-black/60 dark:text-white/60" data-astro-cid-xahix5fp>经验</span> <span class="text-black/80 dark:text-white/80" data-astro-cid-xahix5fp> 1年 6个月 </span> </div> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5" data-astro-cid-xahix5fp> <div class="h-1.5 rounded-full transition-all duration-300" style="width: 60%; background-color: #F24E1E" data-astro-cid-xahix5fp></div> </div> </div> </div> </div> </div> </div> </div> </div> <!-- 技能分布图表 --> <div class="mt-12 pt-8 border-t border-black/10 dark:border-white/10" data-astro-cid-xahix5fp> <h2 class="text-2xl font-bold text-black/90 dark:text-white/90 mb-6" data-astro-cid-xahix5fp> 技能分布 </h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-8" data-astro-cid-xahix5fp> <!-- 按等级分布 --> <div data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/80 dark:text-white/80 mb-4" data-astro-cid-xahix5fp> 按等级分布 </h3> <div class="space-y-3" data-astro-cid-xahix5fp> <div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70" data-astro-cid-xahix5fp> 初级 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-500 bg-green-500" style="width: 0%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 0 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70" data-astro-cid-xahix5fp> 中级 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-500 bg-yellow-500" style="width: 56%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 9 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70" data-astro-cid-xahix5fp> 高级 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-500 bg-orange-500" style="width: 38%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 6 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70" data-astro-cid-xahix5fp> 专家级 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full transition-all duration-500 bg-red-500" style="width: 6%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 1 </div> </div> </div> </div> <!-- 按分类分布 --> <div data-astro-cid-xahix5fp> <h3 class="text-lg font-semibold text-black/80 dark:text-white/80 mb-4" data-astro-cid-xahix5fp> 按分类分布 </h3> <div class="space-y-3" data-astro-cid-xahix5fp> <div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70 truncate" data-astro-cid-xahix5fp> 前端开发 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full bg-blue-500 transition-all duration-500" style="width: 38%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 6 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70 truncate" data-astro-cid-xahix5fp> 后端开发 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full bg-blue-500 transition-all duration-500" style="width: 19%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 3 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70 truncate" data-astro-cid-xahix5fp> 数据库 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full bg-blue-500 transition-all duration-500" style="width: 19%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 3 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70 truncate" data-astro-cid-xahix5fp> 开发工具 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full bg-blue-500 transition-all duration-500" style="width: 25%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 4 </div> </div><div class="flex items-center gap-3" data-astro-cid-xahix5fp> <div class="w-20 text-sm text-black/70 dark:text-white/70 truncate" data-astro-cid-xahix5fp> 其他技能 </div> <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2" data-astro-cid-xahix5fp> <div class="h-2 rounded-full bg-blue-500 transition-all duration-500" style="width: 0%" data-astro-cid-xahix5fp></div> </div> <div class="w-12 text-sm text-black/70 dark:text-white/70 text-right" data-astro-cid-xahix5fp> 0 </div> </div> </div> </div> </div> </div> </div> </div> <div class="footer col-span-2 onload-animation hidden lg:block" data-astro-cid-haiuh7kc> <!--<div class="border-t border-[var(--primary)] mx-16 border-dashed py-8 max-w-[var(--page-width)] flex flex-col items-center justify-center px-6">--><div class="transition border-t border-black/10 dark:border-white/15 my-10 border-dashed mx-32"></div> <!--<div class="transition bg-[oklch(92%_0.01_var(--hue))] dark:bg-black rounded-2xl py-8 mt-4 mb-8 flex flex-col items-center justify-center px-6">--> <div class="transition border-dashed border-[oklch(85%_0.01_var(--hue))] dark:border-white/15 rounded-2xl mb-12 flex flex-col items-center justify-center px-6"> <div class="transition text-50 text-sm text-center">
|
||
© <span id="copyright-year">2026</span> FutureOSS. All Rights Reserved. /
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="/blog/rss.xml">RSS</a> /
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="/blog/sitemap-index.xml">Sitemap</a><br>
|
||
Powered by
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://astro.build">Astro</a> &
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/matsuzaka-yuki/mizuki">Mizuki</a> Version <a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/matsuzaka-yuki/mizuki">4.0<br> </a></div><a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/matsuzaka-yuki/mizuki"></a></div> </div> </div> </main> <div class="footer col-span-2 onload-animation block lg:hidden" data-astro-cid-haiuh7kc> <!--<div class="border-t border-[var(--primary)] mx-16 border-dashed py-8 max-w-[var(--page-width)] flex flex-col items-center justify-center px-6">--><div class="transition border-t border-black/10 dark:border-white/15 my-10 border-dashed mx-32"></div> <!--<div class="transition bg-[oklch(92%_0.01_var(--hue))] dark:bg-black rounded-2xl py-8 mt-4 mb-8 flex flex-col items-center justify-center px-6">--> <div class="transition border-dashed border-[oklch(85%_0.01_var(--hue))] dark:border-white/15 rounded-2xl mb-12 flex flex-col items-center justify-center px-6"> <div class="transition text-50 text-sm text-center">
|
||
© <span id="copyright-year">2026</span> FutureOSS. All Rights Reserved. /
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="/blog/rss.xml">RSS</a> /
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="/blog/sitemap-index.xml">Sitemap</a><br>
|
||
Powered by
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://astro.build">Astro</a> &
|
||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/matsuzaka-yuki/mizuki">Mizuki</a> Version <a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/matsuzaka-yuki/mizuki">4.0<br> </a></div><a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/matsuzaka-yuki/mizuki"></a></div> </div> </div> <!-- There can't be a filter on parent element, or it will break `fixed` --><div class="back-to-top-wrapper hidden lg:block" data-astro-cid-eymb5ayk> <div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()" data-astro-cid-eymb5ayk> <button aria-label="Back to Top" class="btn-card h-[3.75rem] w-[3.75rem]" data-astro-cid-eymb5ayk> <svg width="1em" height="1em" class="mx-auto" data-astro-cid-eymb5ayk="true" data-icon="material-symbols:keyboard-arrow-up-rounded"> <symbol id="ai:material-symbols:keyboard-arrow-up-rounded" viewBox="0 0 24 24"><path fill="currentColor" d="m12 10.8l-3.9 3.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.6-4.6q.3-.3.7-.3t.7.3l4.6 4.6q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></symbol><use href="#ai:material-symbols:keyboard-arrow-up-rounded"></use> </svg> </button> </div> </div> <script>
|
||
function backToTop() {
|
||
// 直接使用原生滚动,避免OverlayScrollbars冲突
|
||
window.scroll({ top: 0, behavior: 'smooth' });
|
||
}
|
||
</script> </div> </div> <div class="absolute w-full z-0 hidden xl:block" data-astro-cid-haiuh7kc> <div class="relative max-w-[var(--page-width)] mx-auto" data-astro-cid-haiuh7kc> <!-- TOC component --> <div id="toc-wrapper" class="hidden lg:block transition absolute top-0 w-[var(--toc-width)] items-center -right-[var(--toc-width)] toc-hide" data-astro-cid-haiuh7kc> <div id="toc-inner-wrapper" class="fixed top-14 w-[var(--toc-width)] h-[calc(100vh_-_20rem)] overflow-y-scroll overflow-x-hidden hide-scrollbar" data-astro-cid-haiuh7kc> <div id="toc" class="w-full h-full transition-swup-fade " data-astro-cid-haiuh7kc> <div class="h-8 w-full" data-astro-cid-haiuh7kc></div> <table-of-contents id="toc" class="group"> <!-- TOC内容将由JavaScript动态生成 --> </table-of-contents> <script type="module" src="/blog/_astro/TOC.astro_astro_type_script_index_0_lang.CGKGUlSz.js"></script> <div class="h-8 w-full" data-astro-cid-haiuh7kc></div> </div> </div> </div> <!-- #toc needs to exist for Swup to work normally --> </div> </div> <!-- Music Player --> <script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event("astro:load"));})();</script><astro-island uid="Z1HtLzS" component-url="/blog/_astro/MusicPlayer.Ckr8jCXu.js" component-export="default" renderer-url="/blog/_astro/client.svelte.9vP7DkWT.js" props="{"data-astro-cid-sckkx6r4":[0,true]}" ssr client="load" opts="{"name":"MusicPlayer","value":true}" await-children><!--[--><!--[-1--><!--]--><!--]--><!--astro:end--></astro-island> <!-- increase the page height during page transition to prevent the scrolling animation from jumping --> <div id="page-height-extend" class="hidden h-[300vh]" data-astro-cid-sckkx6r4 style="--bannerOffset: 15vh;--banner-height-home: 65vh;--banner-height: 35vh;--configHue: 210;--page-width: 90rem;"></div> <!-- Sakura Effect --> <script>(function(){const sakuraConfig = {"enable":true,"sakuraNum":21,"limitTimes":-1,"size":{"min":0.5,"max":1.1},"speed":{"horizontal":{"min":-1.7,"max":-1.2},"vertical":{"min":1.5,"max":2.2},"rotation":0.03},"zIndex":100};
|
||
|
||
// 樱花对象类
|
||
class Sakura {
|
||
constructor(x, y, s, r, fn, idx, img, limitArray, config) {
|
||
this.x = x;
|
||
this.y = y;
|
||
this.s = s;
|
||
this.r = r;
|
||
this.fn = fn;
|
||
this.idx = idx;
|
||
this.img = img;
|
||
this.limitArray = limitArray;
|
||
this.config = config;
|
||
}
|
||
|
||
draw(cxt) {
|
||
cxt.save();
|
||
cxt.translate(this.x, this.y);
|
||
cxt.rotate(this.r);
|
||
cxt.drawImage(this.img, 0, 0, 40 * this.s, 40 * this.s);
|
||
cxt.restore();
|
||
}
|
||
|
||
update() {
|
||
this.x = this.fn.x(this.x, this.y);
|
||
this.y = this.fn.y(this.y, this.y);
|
||
this.r = this.fn.r(this.r);
|
||
|
||
// 如果樱花越界,重新调整位置
|
||
if (
|
||
this.x > window.innerWidth ||
|
||
this.x < 0 ||
|
||
this.y > window.innerHeight ||
|
||
this.y < 0
|
||
) {
|
||
// 如果樱花不做限制
|
||
if (this.limitArray[this.idx] === -1) {
|
||
this.resetPosition();
|
||
}
|
||
// 否则樱花有限制
|
||
else {
|
||
if (this.limitArray[this.idx] > 0) {
|
||
this.resetPosition();
|
||
this.limitArray[this.idx]--;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
resetPosition() {
|
||
this.r = getRandom('fnr', this.config);
|
||
if (Math.random() > 0.4) {
|
||
this.x = getRandom('x', this.config);
|
||
this.y = 0;
|
||
this.s = getRandom('s', this.config);
|
||
this.r = getRandom('r', this.config);
|
||
} else {
|
||
this.x = window.innerWidth;
|
||
this.y = getRandom('y', this.config);
|
||
this.s = getRandom('s', this.config);
|
||
this.r = getRandom('r', this.config);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 樱花列表类
|
||
class SakuraList {
|
||
constructor() {
|
||
this.list = [];
|
||
}
|
||
|
||
push(sakura) {
|
||
this.list.push(sakura);
|
||
}
|
||
|
||
update() {
|
||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||
this.list[i].update();
|
||
}
|
||
}
|
||
|
||
draw(cxt) {
|
||
for (let i = 0, len = this.list.length; i < len; i++) {
|
||
this.list[i].draw(cxt);
|
||
}
|
||
}
|
||
|
||
get(i) {
|
||
return this.list[i];
|
||
}
|
||
|
||
size() {
|
||
return this.list.length;
|
||
}
|
||
}
|
||
|
||
// 获取随机值的函数
|
||
function getRandom(option, config) {
|
||
let ret;
|
||
let random;
|
||
|
||
switch (option) {
|
||
case 'x':
|
||
ret = Math.random() * window.innerWidth;
|
||
break;
|
||
case 'y':
|
||
ret = Math.random() * window.innerHeight;
|
||
break;
|
||
case 's':
|
||
ret = config.size.min + Math.random() * (config.size.max - config.size.min);
|
||
break;
|
||
case 'r':
|
||
ret = Math.random() * 6;
|
||
break;
|
||
case 'fnx':
|
||
random = config.speed.horizontal.min + Math.random() * (config.speed.horizontal.max - config.speed.horizontal.min);
|
||
ret = function (x, y) {
|
||
return x + random;
|
||
};
|
||
break;
|
||
case 'fny':
|
||
random = config.speed.vertical.min + Math.random() * (config.speed.vertical.max - config.speed.vertical.min);
|
||
ret = function (x, y) {
|
||
return y + random;
|
||
};
|
||
break;
|
||
case 'fnr':
|
||
ret = function (r) {
|
||
return r + config.speed.rotation;
|
||
};
|
||
break;
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
// 樱花管理器类
|
||
class SakuraManager {
|
||
constructor(config) {
|
||
this.config = config;
|
||
this.canvas = null;
|
||
this.ctx = null;
|
||
this.sakuraList = null;
|
||
this.animationId = null;
|
||
this.img = null;
|
||
this.isRunning = false;
|
||
}
|
||
|
||
// 初始化樱花特效
|
||
async init() {
|
||
if (!this.config.enable || this.isRunning) {
|
||
return;
|
||
}
|
||
|
||
// 创建图片对象
|
||
this.img = new Image();
|
||
this.img.src = '/sakura.png'; // 使用樱花图片
|
||
|
||
// 等待图片加载完成
|
||
await new Promise((resolve, reject) => {
|
||
if (this.img) {
|
||
this.img.onload = () => resolve();
|
||
this.img.onerror = () => reject(new Error('Failed to load sakura image'));
|
||
}
|
||
});
|
||
|
||
this.createCanvas();
|
||
this.createSakuraList();
|
||
this.startAnimation();
|
||
this.isRunning = true;
|
||
}
|
||
|
||
// 创建画布
|
||
createCanvas() {
|
||
this.canvas = document.createElement('canvas');
|
||
this.canvas.height = window.innerHeight;
|
||
this.canvas.width = window.innerWidth;
|
||
this.canvas.setAttribute('style', `position: fixed; left: 0; top: 0; pointer-events: none; z-index: ${this.config.zIndex};`);
|
||
this.canvas.setAttribute('id', 'canvas_sakura');
|
||
document.body.appendChild(this.canvas);
|
||
this.ctx = this.canvas.getContext('2d');
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', this.handleResize.bind(this));
|
||
}
|
||
|
||
// 创建樱花列表
|
||
createSakuraList() {
|
||
if (!this.img || !this.ctx) return;
|
||
|
||
this.sakuraList = new SakuraList();
|
||
const limitArray = new Array(this.config.sakuraNum).fill(this.config.limitTimes);
|
||
|
||
for (let i = 0; i < this.config.sakuraNum; i++) {
|
||
const randomX = getRandom('x', this.config);
|
||
const randomY = getRandom('y', this.config);
|
||
const randomR = getRandom('r', this.config);
|
||
const randomS = getRandom('s', this.config);
|
||
const randomFnx = getRandom('fnx', this.config);
|
||
const randomFny = getRandom('fny', this.config);
|
||
const randomFnR = getRandom('fnr', this.config);
|
||
|
||
const sakura = new Sakura(
|
||
randomX,
|
||
randomY,
|
||
randomS,
|
||
randomR,
|
||
{
|
||
x: randomFnx,
|
||
y: randomFny,
|
||
r: randomFnR,
|
||
},
|
||
i,
|
||
this.img,
|
||
limitArray,
|
||
this.config
|
||
);
|
||
|
||
sakura.draw(this.ctx);
|
||
this.sakuraList.push(sakura);
|
||
}
|
||
}
|
||
|
||
// 开始动画
|
||
startAnimation() {
|
||
if (!this.ctx || !this.canvas || !this.sakuraList) return;
|
||
|
||
const animate = () => {
|
||
if (!this.ctx || !this.canvas || !this.sakuraList) return;
|
||
|
||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
this.sakuraList.update();
|
||
this.sakuraList.draw(this.ctx);
|
||
this.animationId = requestAnimationFrame(animate);
|
||
};
|
||
|
||
this.animationId = requestAnimationFrame(animate);
|
||
}
|
||
|
||
// 处理窗口大小变化
|
||
handleResize() {
|
||
if (this.canvas) {
|
||
this.canvas.width = window.innerWidth;
|
||
this.canvas.height = window.innerHeight;
|
||
}
|
||
}
|
||
|
||
// 停止樱花特效
|
||
stop() {
|
||
if (this.animationId) {
|
||
cancelAnimationFrame(this.animationId);
|
||
this.animationId = null;
|
||
}
|
||
|
||
if (this.canvas) {
|
||
document.body.removeChild(this.canvas);
|
||
this.canvas = null;
|
||
}
|
||
|
||
window.removeEventListener('resize', this.handleResize.bind(this));
|
||
this.isRunning = false;
|
||
}
|
||
|
||
// 切换樱花特效
|
||
toggle() {
|
||
if (this.isRunning) {
|
||
this.stop();
|
||
} else {
|
||
this.init();
|
||
}
|
||
}
|
||
|
||
// 更新配置
|
||
updateConfig(newConfig) {
|
||
const wasRunning = this.isRunning;
|
||
if (wasRunning) {
|
||
this.stop();
|
||
}
|
||
this.config = newConfig;
|
||
if (wasRunning && newConfig.enable) {
|
||
this.init();
|
||
}
|
||
}
|
||
|
||
// 获取运行状态
|
||
getIsRunning() {
|
||
return this.isRunning;
|
||
}
|
||
}
|
||
|
||
// 创建全局樱花管理器实例
|
||
let globalSakuraManager = null;
|
||
|
||
// 初始化樱花特效
|
||
function initSakura(config) {
|
||
if (globalSakuraManager) {
|
||
globalSakuraManager.updateConfig(config);
|
||
} else {
|
||
globalSakuraManager = new SakuraManager(config);
|
||
if (config.enable) {
|
||
globalSakuraManager.init();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 樱花特效初始化
|
||
(function() {
|
||
// 全局标记,确保樱花特效只初始化一次
|
||
if (window.sakuraInitialized) {
|
||
return;
|
||
}
|
||
|
||
// 初始化樱花特效的函数
|
||
const setupSakura = () => {
|
||
if (sakuraConfig.enable && !window.sakuraInitialized) {
|
||
initSakura(sakuraConfig);
|
||
window.sakuraInitialized = true;
|
||
}
|
||
};
|
||
|
||
// 页面加载完成后初始化樱花特效
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', setupSakura);
|
||
} else {
|
||
setupSakura();
|
||
}
|
||
})();
|
||
})();</script> <!-- Translate.js integration - lazy loading --> <script type="module">class d{constructor(){this.isInitialized=!1,this.clickHandler=null}init(){console.log("GalleryManager init called");const t=document.querySelectorAll(".gallery-group");if(console.log("Found gallery groups:",t.length),t.length===0){console.log("No gallery groups found, skipping initialization");return}this.cleanup(),console.log("Adding click event listener to document"),this.clickHandler=e=>this.handleClick(e),document.addEventListener("click",this.clickHandler),this.isInitialized=!0}cleanup(){this.clickHandler&&(document.removeEventListener("click",this.clickHandler),this.clickHandler=null),this.isInitialized=!1}handleClick(t){console.log("GalleryManager handleClick triggered",t.target);const e=t.target.closest(".gallery-header");if(e){console.log("Gallery header clicked",e),t.preventDefault(),t.stopPropagation(),this.toggleGallery(e);return}const n=t.target.closest(".gallery-image");if(n){console.log("Gallery image clicked",n),t.preventDefault(),t.stopPropagation();const l=n.getAttribute("data-image");l&&this.showImageModal(l)}}toggleGallery(t){console.log("toggleGallery called",t);const e=t.closest(".gallery-group"),n=e.querySelector(".gallery-content"),l=t.querySelector(".expand-icon");console.log("Found elements:",{group:e,content:n,icon:l}),console.log("Content hidden?",n?.classList.contains("hidden")),n&&n.classList.contains("hidden")?(console.log("Expanding gallery"),n.classList.remove("hidden"),e.classList.add("expanded"),l&&(l.style.transform="rotate(180deg)")):n?(console.log("Collapsing gallery"),n.classList.add("hidden"),e.classList.remove("expanded"),l&&(l.style.transform="rotate(0deg)")):console.error("Gallery content not found!")}showImageModal(t){const e=document.createElement("div");e.className="gallery-modal fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4",e.style.opacity="0",e.style.transition="opacity 0.3s ease";const n=document.createElement("div");n.className="relative max-w-full max-h-full";const l=document.createElement("img");l.src=t,l.alt="查看图片",l.className="max-w-full max-h-full object-contain rounded-lg",l.style.transform="scale(0.9)",l.style.transition="transform 0.3s ease";const o=document.createElement("button");o.className="close-btn absolute top-4 right-4 bg-black bg-opacity-50 text-white p-2 rounded-full hover:bg-opacity-70 transition-colors",o.innerHTML=`
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||
</svg>
|
||
`,n.appendChild(l),n.appendChild(o),e.appendChild(n),document.body.appendChild(e),requestAnimationFrame(()=>{e.style.opacity="1",l.style.transform="scale(1)"});const a=()=>{e.style.opacity="0",l.style.transform="scale(0.9)",setTimeout(()=>{document.body.contains(e)&&document.body.removeChild(e),document.removeEventListener("keydown",c)},300)},c=s=>{s.key==="Escape"&&a()};o.addEventListener("click",a),e.addEventListener("click",s=>{s.target===e&&a()}),document.addEventListener("keydown",c)}}window.galleryManager=new d;const r=()=>{window.galleryManager.init()},i=()=>{r(),window.swup&&(window.swup.hooks.on("page:view",()=>{setTimeout(r,50)}),window.swup.hooks.on("content:replace",()=>{window.galleryManager.cleanup()},{before:!0}))};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",i):i();window.swup||document.addEventListener("swup:enable",i);</script> </body> </html> <script type="module" src="/blog/_astro/Layout.astro_astro_type_script_index_1_lang.BilWV2C2.js"></script> <script type="module" src="/blog/_astro/Layout.astro_astro_type_script_index_2_lang.BTdkr2U1.js"></script> <script type="module">window.loadTranslateScript=function(){return window.translate||document.getElementById("translate-script")?Promise.resolve():new Promise((n,t)=>{const e=document.createElement("script");e.src="/translate.js",e.id="translate-script",e.async=!0,e.onload=()=>{typeof window.translate<"u"?n():t(new Error("translate.js loaded but window.translate not available"))},e.onerror=t,document.head.appendChild(e)})};</script> <script type="module">document.addEventListener("DOMContentLoaded",()=>{window.__iconifyLoader&&window.__iconifyLoader.load().then(()=>{document.querySelectorAll(".skill-card").forEach(e=>{e.dispatchEvent(new CustomEvent("iconify-ready"))})}).catch(o=>{console.error("Failed to load icons on skills page:",o)})});typeof window<"u"&&window.addEventListener("pageshow",o=>{o.persisted&&window.__iconifyLoader&&setTimeout(()=>{window.__iconifyLoader.load().catch(console.error)},100)});</script> |