feat: Refactor service management scripts to use a unified dev script
- Added package.json to manage development scripts. - Updated restart-services.ps1 to call the new dev script for starting services. - Refactored start-admin.ps1, start-backend.ps1, start-frontend.ps1, and start-mcp.ps1 to utilize the dev script for starting respective services. - Enhanced stop-services.ps1 to improve process termination logic by matching command patterns.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import { API_BASE_URL } from '../lib/api/client';
|
||||
import { terminalConfig } from '../lib/config/terminal';
|
||||
import { getI18n, SUPPORTED_LOCALES } from '../lib/i18n';
|
||||
import type { SiteSettings } from '../lib/types';
|
||||
@@ -14,6 +15,12 @@ const {
|
||||
|
||||
const { locale, t, buildLocaleUrl } = getI18n(Astro);
|
||||
const aiEnabled = Boolean(Astro.props.siteSettings?.ai?.enabled);
|
||||
const musicPlaylist = (Astro.props.siteSettings?.musicPlaylist || []).filter(
|
||||
(item) => item?.title?.trim() && item?.url?.trim()
|
||||
);
|
||||
const musicPlaylistPayload = JSON.stringify(musicPlaylist);
|
||||
const hasMusicPlaylist = musicPlaylist.length > 0;
|
||||
const currentMusicTrack = hasMusicPlaylist ? musicPlaylist[0] : null;
|
||||
const navItems = [
|
||||
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
||||
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
||||
@@ -31,54 +38,34 @@ const localeLinks = SUPPORTED_LOCALES.map((item) => ({
|
||||
shortLabel: item === 'zh-CN' ? '中' : 'EN',
|
||||
}));
|
||||
const currentPath = Astro.url.pathname;
|
||||
const currentNavLabel =
|
||||
navItems.find((item) => currentPath === item.href || (item.href !== '/' && currentPath.startsWith(item.href)))
|
||||
?.text || t('header.navigation');
|
||||
---
|
||||
|
||||
<header data-ai-search-enabled={aiEnabled ? 'true' : 'false'} class="sticky top-0 z-50 border-b border-[var(--border-color)] backdrop-blur-xl" style="background-color: color-mix(in oklab, var(--bg) 88%, transparent);">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5">
|
||||
<div class="terminal-toolbar-shell">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/" class="terminal-toolbar-module shrink-0 min-w-[11.5rem] hover:border-[var(--primary)] transition-all">
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-xl border border-[var(--border-color)] bg-[var(--primary)]/8 text-[var(--primary)]">
|
||||
<i class="fas fa-terminal text-lg"></i>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex items-center gap-2 lg:flex-nowrap">
|
||||
<a href="/" class="terminal-toolbar-module shrink-0 min-w-[9.5rem] px-2.5 py-1.5 hover:border-[var(--primary)] transition-all">
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-xl border border-[var(--border-color)] bg-[var(--primary)]/8 text-[var(--primary)]">
|
||||
<i class="fas fa-terminal text-base"></i>
|
||||
</span>
|
||||
<span>
|
||||
<span class="terminal-toolbar-label block">root@termi</span>
|
||||
<span class="mt-1 block text-lg font-bold text-[var(--title-color)]">{siteName}</span>
|
||||
<span class="terminal-toolbar-label block">{t('header.shellLabel')}</span>
|
||||
<span class="mt-0.5 block text-[15px] font-bold text-[var(--title-color)]">{siteName}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="hidden xl:flex terminal-toolbar-module min-w-[15rem]">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="terminal-toolbar-label">playerctl</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<button id="music-prev" class="terminal-toolbar-iconbtn">
|
||||
<i class="fas fa-step-backward text-xs"></i>
|
||||
</button>
|
||||
<button id="music-play" class="terminal-toolbar-iconbtn bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));">
|
||||
<i class="fas fa-play text-xs" id="music-play-icon"></i>
|
||||
</button>
|
||||
<button id="music-next" class="terminal-toolbar-iconbtn">
|
||||
<i class="fas fa-step-forward text-xs"></i>
|
||||
</button>
|
||||
<span class="min-w-0 flex-1 truncate text-xs font-mono text-[var(--text-secondary)]" id="music-title">
|
||||
ギターと孤独と蒼い惑星
|
||||
</span>
|
||||
<button id="music-volume" class="terminal-toolbar-iconbtn">
|
||||
<i class="fas fa-volume-up text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative hidden md:block flex-1 min-w-0">
|
||||
<div class="terminal-toolbar-module gap-3">
|
||||
<div class="terminal-toolbar-label" id="search-label">grep -i</div>
|
||||
<div class="relative hidden lg:block flex-1 min-w-0 max-w-[16rem] xl:max-w-[18rem]">
|
||||
<div class="terminal-toolbar-module gap-2 px-2.5 py-1.5">
|
||||
<div class="terminal-toolbar-label" id="search-label">{t('header.searchPromptKeyword')}</div>
|
||||
{aiEnabled && (
|
||||
<div id="search-mode-panel" class="flex items-center gap-1 rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<div id="search-mode-panel" class="hidden 2xl:flex items-center gap-1 rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn rounded-lg px-3 py-2 text-xs font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
class="search-mode-btn rounded-lg px-2.5 py-1.5 text-xs font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="keyword"
|
||||
aria-pressed="true"
|
||||
>
|
||||
@@ -87,7 +74,7 @@ const currentPath = Astro.url.pathname;
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn rounded-lg px-3 py-2 text-xs font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
class="search-mode-btn rounded-lg px-2.5 py-1.5 text-xs font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="ai"
|
||||
aria-pressed="false"
|
||||
>
|
||||
@@ -102,24 +89,73 @@ const currentPath = Astro.url.pathname;
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<span id="search-hint" class="hidden xl:inline text-xs font-mono text-[var(--secondary)]">articles/*.md</span>
|
||||
<button id="search-btn" class="terminal-toolbar-iconbtn">
|
||||
<button id="search-btn" class="terminal-toolbar-iconbtn h-8 w-8">
|
||||
<i id="search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p id="search-hint" class="px-1 pt-0.5 text-[11px] font-mono text-[var(--text-tertiary)]">
|
||||
{t('header.searchHintKeyword')}
|
||||
</p>
|
||||
<div
|
||||
id="search-results"
|
||||
class="hidden absolute right-0 top-[calc(100%+12px)] w-[26rem] overflow-hidden rounded-xl border border-[var(--border-color)] bg-[var(--terminal-bg)] shadow-[0_20px_40px_rgba(15,23,42,0.08)]"
|
||||
class="hidden absolute right-0 top-[calc(100%+12px)] z-20 w-[26rem] overflow-hidden rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)] shadow-[0_20px_40px_rgba(15,23,42,0.08)]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex items-center gap-1 rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<div class="hidden 2xl:flex terminal-toolbar-module min-w-0 max-w-[13rem] gap-2 px-2.5 py-1.5">
|
||||
<div class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-color)] bg-[var(--primary)]/8">
|
||||
<img
|
||||
id="desktop-music-cover"
|
||||
src={currentMusicTrack?.coverImageUrl || ''}
|
||||
alt={currentMusicTrack?.title || 'Music cover'}
|
||||
class:list={[
|
||||
'h-full w-full object-cover',
|
||||
!currentMusicTrack?.coverImageUrl && 'hidden'
|
||||
]}
|
||||
/>
|
||||
<i
|
||||
id="desktop-music-cover-fallback"
|
||||
class:list={[
|
||||
'fas fa-compact-disc text-sm text-[var(--primary)]',
|
||||
currentMusicTrack?.coverImageUrl && 'hidden'
|
||||
]}
|
||||
></i>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-[13px] font-semibold text-[var(--title-color)]" id="desktop-music-title">
|
||||
{currentMusicTrack?.title || '未配置曲目'}
|
||||
</p>
|
||||
<div class="mt-1 flex items-center gap-1">
|
||||
<button id="desktop-music-prev" class="terminal-toolbar-iconbtn h-7 w-7" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-backward text-[11px]"></i>
|
||||
</button>
|
||||
<button id="desktop-music-play" class="terminal-toolbar-iconbtn h-7 w-7 bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-play text-[11px]" id="desktop-music-play-icon"></i>
|
||||
</button>
|
||||
<button id="desktop-music-next" class="terminal-toolbar-iconbtn h-7 w-7" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-forward text-[11px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiEnabled && (
|
||||
<a
|
||||
href="/ask"
|
||||
class="hidden lg:inline-flex items-center gap-2 rounded-xl border border-[var(--primary)]/18 bg-[var(--primary)]/8 px-2.5 py-1.5 text-sm font-medium text-[var(--primary)] transition hover:border-[var(--primary)]/32 hover:text-[var(--title-color)]"
|
||||
>
|
||||
<i class="fas fa-robot text-sm"></i>
|
||||
<span class="hidden xl:inline">{t('nav.ask')}</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div class="hidden lg:flex items-center gap-1 rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-0.5">
|
||||
{localeLinks.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
data-locale-switch={item.locale}
|
||||
class:list={[
|
||||
'rounded-lg px-3 py-2 text-xs font-semibold transition',
|
||||
'rounded-lg px-2.5 py-1.5 text-xs font-semibold transition',
|
||||
item.locale === locale
|
||||
? 'bg-[var(--primary)] text-white shadow-sm'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--header-bg)]'
|
||||
@@ -132,169 +168,248 @@ const currentPath = Astro.url.pathname;
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="theme-toggle terminal-toolbar-iconbtn h-11 w-11 shrink-0"
|
||||
aria-label={t('header.themeToggle')}
|
||||
title={t('header.themeToggle')}
|
||||
>
|
||||
<i id="theme-icon" class="fas fa-moon text-[var(--primary)]"></i>
|
||||
</button>
|
||||
<div class="relative shrink-0">
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="theme-toggle terminal-toolbar-iconbtn h-8 w-8 shrink-0"
|
||||
aria-label={t('header.themeToggle')}
|
||||
title={t('header.themeToggle')}
|
||||
>
|
||||
<i id="theme-icon" class="fas fa-desktop text-sm text-[var(--text-secondary)]"></i>
|
||||
<span id="theme-toggle-label" class="sr-only">
|
||||
{t('header.themeSystem')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="mobile-menu-btn"
|
||||
class="lg:hidden terminal-toolbar-iconbtn h-11 w-11 shrink-0"
|
||||
class="terminal-toolbar-iconbtn h-9 w-9 shrink-0 lg:hidden"
|
||||
aria-label={t('header.toggleMenu')}
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">
|
||||
{currentNavLabel}
|
||||
</span>
|
||||
<i class="fas fa-bars text-[var(--text)]"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex items-center gap-3 border-t border-[var(--border-color)]/70 pt-3">
|
||||
<div class="terminal-toolbar-label">{t('header.navigation')}</div>
|
||||
<nav class="min-w-0 flex-1 flex items-center gap-1.5 overflow-x-auto pb-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentPath === item.href || (item.href !== '/' && currentPath.startsWith(item.href));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
'terminal-nav-link',
|
||||
isActive && 'is-active'
|
||||
]}
|
||||
>
|
||||
<i class={`fas ${item.icon} text-xs`}></i>
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobile-menu" class="hidden lg:hidden border-t border-[var(--border-color)] bg-[var(--bg)]">
|
||||
<div class="px-4 py-3 space-y-3">
|
||||
<div class="space-y-3 md:hidden">
|
||||
{aiEnabled && (
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn flex-1 rounded-xl px-3 py-2 text-sm font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="keyword"
|
||||
aria-pressed="true"
|
||||
>
|
||||
<i class="fas fa-search mr-2 text-xs"></i>
|
||||
<span>{t('header.searchModeKeywordMobile')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn flex-1 rounded-xl px-3 py-2 text-sm font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="ai"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<i class="fas fa-robot mr-2 text-xs"></i>
|
||||
<span>{t('header.searchModeAiMobile')}</span>
|
||||
</button>
|
||||
<div id="mobile-menu" class="hidden border-t border-[var(--border-color)] bg-[var(--bg)] lg:hidden">
|
||||
<div class="px-4 py-4 space-y-4">
|
||||
<div class="grid gap-3 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3 lg:hidden">
|
||||
{aiEnabled && (
|
||||
<div class="flex items-center gap-2 rounded-2xl border border-[var(--border-color)] bg-[var(--header-bg)]/80 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn flex-1 rounded-xl px-3 py-2 text-sm font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="keyword"
|
||||
aria-pressed="true"
|
||||
>
|
||||
<i class="fas fa-search mr-2 text-xs"></i>
|
||||
<span>{t('header.searchModeKeywordMobile')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="search-mode-btn flex-1 rounded-xl px-3 py-2 text-sm font-medium text-[var(--text-secondary)] transition hover:bg-[var(--header-bg)]"
|
||||
data-search-mode="ai"
|
||||
aria-pressed="false"
|
||||
>
|
||||
<i class="fas fa-robot mr-2 text-xs"></i>
|
||||
<span>{t('header.searchModeAiMobile')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div class="terminal-toolbar-module">
|
||||
<span class="terminal-toolbar-label" id="mobile-search-label">{t('header.searchPromptKeyword')}</span>
|
||||
<input
|
||||
type="text"
|
||||
id="mobile-search-input"
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<button id="mobile-search-btn" class="terminal-toolbar-iconbtn">
|
||||
<i id="mobile-search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p id="mobile-search-hint" class="px-1 text-xs font-mono text-[var(--text-tertiary)]">{t('header.searchHintKeyword')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="terminal-toolbar-label">{t('common.language')}</span>
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
{localeLinks.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
data-locale-switch={item.locale}
|
||||
class:list={[
|
||||
'flex-1 rounded-xl border px-3 py-2 text-center text-sm font-medium transition',
|
||||
item.locale === locale
|
||||
? 'border-[var(--primary)] bg-[var(--primary)]/10 text-[var(--primary)]'
|
||||
: 'border-[var(--border-color)] bg-[var(--header-bg)] text-[var(--text-secondary)]'
|
||||
]}
|
||||
aria-current={item.locale === locale ? 'true' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
<div class="flex items-center gap-2 xl:hidden">
|
||||
<span class="terminal-toolbar-label">{t('common.language')}</span>
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
{localeLinks.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
data-locale-switch={item.locale}
|
||||
class:list={[
|
||||
'flex-1 rounded-xl border px-3 py-2 text-center text-sm font-medium transition',
|
||||
item.locale === locale
|
||||
? 'border-[var(--primary)] bg-[var(--primary)]/10 text-[var(--primary)]'
|
||||
: 'border-[var(--border-color)] bg-[var(--header-bg)] text-[var(--text-secondary)]'
|
||||
]}
|
||||
aria-current={item.locale === locale ? 'true' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-toolbar-module">
|
||||
<span class="terminal-toolbar-label" id="mobile-search-label">grep -i</span>
|
||||
<input
|
||||
type="text"
|
||||
id="mobile-search-input"
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<button id="mobile-search-btn" class="terminal-toolbar-iconbtn">
|
||||
<i id="mobile-search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
|
||||
<div class="terminal-toolbar-module items-center gap-3">
|
||||
<div class="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-[var(--border-color)] bg-[var(--primary)]/8">
|
||||
<img
|
||||
id="music-cover"
|
||||
src={currentMusicTrack?.coverImageUrl || ''}
|
||||
alt={currentMusicTrack?.title || 'Music cover'}
|
||||
class:list={[
|
||||
'h-full w-full object-cover',
|
||||
!currentMusicTrack?.coverImageUrl && 'hidden'
|
||||
]}
|
||||
/>
|
||||
<i
|
||||
id="music-cover-fallback"
|
||||
class:list={[
|
||||
'fas fa-compact-disc text-base text-[var(--primary)]',
|
||||
currentMusicTrack?.coverImageUrl && 'hidden'
|
||||
]}
|
||||
></i>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="terminal-toolbar-label">{t('header.musicPanel')}</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<button id="music-prev" class="terminal-toolbar-iconbtn" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-backward text-xs"></i>
|
||||
</button>
|
||||
<button id="music-play" class="terminal-toolbar-iconbtn bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-play text-xs" id="music-play-icon"></i>
|
||||
</button>
|
||||
<button id="music-next" class="terminal-toolbar-iconbtn" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-forward text-xs"></i>
|
||||
</button>
|
||||
<button id="music-volume" class="terminal-toolbar-iconbtn" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-volume-up text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-[var(--title-color)]" id="music-title">
|
||||
{currentMusicTrack?.title || '未配置曲目'}
|
||||
</p>
|
||||
<p class="truncate text-[11px] text-[var(--text-tertiary)]" id="music-artist">
|
||||
{currentMusicTrack?.artist || currentMusicTrack?.album || '等待播放'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="mobile-search-hint" class="px-1 text-xs font-mono text-[var(--text-tertiary)]">articles/*.md</p>
|
||||
</div>
|
||||
{navItems.map(item => (
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
'terminal-nav-link flex',
|
||||
currentPath === item.href || (item.href !== '/' && currentPath.startsWith(item.href))
|
||||
? 'is-active'
|
||||
: ''
|
||||
]}
|
||||
>
|
||||
<i class={`fas ${item.icon} w-5`}></i>
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
))}
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{navItems.map(item => (
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
'terminal-nav-link',
|
||||
currentPath === item.href || (item.href !== '/' && currentPath.startsWith(item.href))
|
||||
? 'is-active'
|
||||
: ''
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-3">
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-xl border border-[var(--border-color)] bg-[var(--header-bg)]/82 text-[var(--primary)]">
|
||||
<i class={`fas ${item.icon} text-sm`}></i>
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="terminal-toolbar-label block">{t('header.navigation')}</span>
|
||||
<span class="mt-1 block text-sm font-semibold text-[var(--title-color)]">{item.text}</span>
|
||||
</span>
|
||||
</span>
|
||||
<i class="fas fa-arrow-right text-[11px] text-[var(--text-tertiary)]"></i>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script is:inline>
|
||||
// Theme Toggle - simplified vanilla JS
|
||||
<script is:inline define:vars={{ apiBase: API_BASE_URL, musicPlaylistPayload }}>
|
||||
const t = window.__termiTranslate;
|
||||
|
||||
// Theme selection
|
||||
function initThemeToggle() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeIcon = document.getElementById('theme-icon');
|
||||
const themeToggleLabel = document.getElementById('theme-toggle-label');
|
||||
const themeApi = window.__termiTheme;
|
||||
|
||||
if (!themeToggle || !themeIcon) {
|
||||
console.error('[Theme] Elements not found');
|
||||
if (!themeToggle || !themeIcon || !themeApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Theme] Initializing toggle button');
|
||||
|
||||
function updateThemeIcon(isDark) {
|
||||
console.log('[Theme] Updating icon, isDark:', isDark);
|
||||
if (isDark) {
|
||||
themeIcon.className = 'fas fa-sun text-[var(--secondary)]';
|
||||
} else {
|
||||
themeIcon.className = 'fas fa-moon text-[var(--primary)]';
|
||||
}
|
||||
if (themeToggle.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
console.log('[Theme] Button clicked');
|
||||
const root = document.documentElement;
|
||||
const hasDark = root.classList.contains('dark');
|
||||
console.log('[Theme] Current hasDark:', hasDark);
|
||||
themeToggle.dataset.bound = 'true';
|
||||
|
||||
if (hasDark) {
|
||||
root.classList.remove('dark');
|
||||
root.classList.add('light');
|
||||
localStorage.setItem('theme', 'light');
|
||||
updateThemeIcon(false);
|
||||
} else {
|
||||
root.classList.remove('light');
|
||||
root.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
updateThemeIcon(true);
|
||||
const themeMeta = {
|
||||
light: {
|
||||
iconClass: 'fas fa-sun text-sm',
|
||||
color: 'var(--secondary)',
|
||||
label: t('header.themeLight'),
|
||||
},
|
||||
dark: {
|
||||
iconClass: 'fas fa-moon text-sm',
|
||||
color: 'var(--primary)',
|
||||
label: t('header.themeDark'),
|
||||
},
|
||||
system: {
|
||||
iconClass: 'fas fa-desktop text-sm',
|
||||
color: 'var(--text-secondary)',
|
||||
label: t('header.themeSystem'),
|
||||
},
|
||||
};
|
||||
|
||||
function updateThemeUI(detail = null) {
|
||||
const mode = detail?.mode || themeApi.getMode();
|
||||
const resolved = detail?.resolved || themeApi.resolveTheme(mode);
|
||||
const modeMeta = themeMeta[mode] || themeMeta.system;
|
||||
const modeLabel = themeMeta[mode]?.label || themeMeta.system.label;
|
||||
const resolvedLabel = resolved === 'dark' ? t('header.themeDark') : t('header.themeLight');
|
||||
|
||||
themeIcon.className = modeMeta.iconClass;
|
||||
themeIcon.style.color = modeMeta.color;
|
||||
|
||||
if (themeToggleLabel) {
|
||||
themeToggleLabel.textContent = `${modeLabel} / ${resolvedLabel}`;
|
||||
}
|
||||
|
||||
const toggleTitle = `${t('header.themeToggle')} · ${modeLabel} / ${resolvedLabel}`;
|
||||
themeToggle.setAttribute('aria-label', toggleTitle);
|
||||
themeToggle.setAttribute('title', toggleTitle);
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentMode = themeApi.getMode();
|
||||
const nextMode =
|
||||
currentMode === 'system' ? 'light' :
|
||||
currentMode === 'light' ? 'dark' :
|
||||
'system';
|
||||
themeApi.applyTheme(nextMode);
|
||||
});
|
||||
|
||||
// Initialize icon based on current theme
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
updateThemeIcon(isDark);
|
||||
window.addEventListener('termi:theme-change', function(event) {
|
||||
updateThemeUI(event.detail);
|
||||
});
|
||||
|
||||
updateThemeUI(themeApi.syncTheme());
|
||||
}
|
||||
|
||||
// Run immediately if DOM is ready, otherwise wait
|
||||
@@ -304,30 +419,44 @@ const currentPath = Astro.url.pathname;
|
||||
initThemeToggle();
|
||||
}
|
||||
|
||||
// Mobile Menu
|
||||
// Site Menu
|
||||
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileSearchInput = document.getElementById('mobile-search-input');
|
||||
const mobileSearchBtn = document.getElementById('mobile-search-btn');
|
||||
|
||||
mobileMenuBtn?.addEventListener('click', () => {
|
||||
const nextExpanded = mobileMenu?.classList.contains('hidden');
|
||||
mobileMenu?.classList.toggle('hidden');
|
||||
mobileMenuBtn.setAttribute('aria-expanded', String(nextExpanded));
|
||||
});
|
||||
|
||||
document.querySelectorAll('#mobile-menu a[href]').forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileMenu?.classList.add('hidden');
|
||||
mobileMenuBtn?.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
// Music Player with actual audio
|
||||
const musicPlay = document.getElementById('music-play');
|
||||
const musicPlayIcon = document.getElementById('music-play-icon');
|
||||
const musicCover = document.getElementById('music-cover');
|
||||
const musicCoverFallback = document.getElementById('music-cover-fallback');
|
||||
const musicTitle = document.getElementById('music-title');
|
||||
const musicArtist = document.getElementById('music-artist');
|
||||
const musicPrev = document.getElementById('music-prev');
|
||||
const musicNext = document.getElementById('music-next');
|
||||
const musicVolume = document.getElementById('music-volume');
|
||||
const desktopMusicPlay = document.getElementById('desktop-music-play');
|
||||
const desktopMusicPlayIcon = document.getElementById('desktop-music-play-icon');
|
||||
const desktopMusicCover = document.getElementById('desktop-music-cover');
|
||||
const desktopMusicCoverFallback = document.getElementById('desktop-music-cover-fallback');
|
||||
const desktopMusicTitle = document.getElementById('desktop-music-title');
|
||||
const desktopMusicPrev = document.getElementById('desktop-music-prev');
|
||||
const desktopMusicNext = document.getElementById('desktop-music-next');
|
||||
|
||||
// Playlist - Using placeholder audio URLs (replace with actual music URLs)
|
||||
const playlist = [
|
||||
{ title: 'ギターと孤独と蒼い惑星', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' },
|
||||
{ title: '星座になれたら', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3' },
|
||||
{ title: 'あのバンド', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3' }
|
||||
];
|
||||
const playlist = JSON.parse(musicPlaylistPayload || '[]');
|
||||
|
||||
let currentSongIndex = 0;
|
||||
let isPlaying = false;
|
||||
@@ -345,12 +474,46 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
const currentTrack = playlist[currentSongIndex] || {};
|
||||
if (musicTitle) {
|
||||
musicTitle.textContent = playlist[currentSongIndex].title;
|
||||
musicTitle.textContent = currentTrack.title || '未配置曲目';
|
||||
}
|
||||
if (desktopMusicTitle) {
|
||||
desktopMusicTitle.textContent = currentTrack.title || '未配置曲目';
|
||||
}
|
||||
if (musicArtist) {
|
||||
musicArtist.textContent = currentTrack.artist || currentTrack.album || '等待播放';
|
||||
}
|
||||
if (musicCover && musicCoverFallback) {
|
||||
if (currentTrack.coverImageUrl) {
|
||||
musicCover.setAttribute('src', currentTrack.coverImageUrl);
|
||||
musicCover.setAttribute('alt', currentTrack.title || 'Music cover');
|
||||
musicCover.classList.remove('hidden');
|
||||
musicCoverFallback.classList.add('hidden');
|
||||
} else {
|
||||
musicCover.setAttribute('src', '');
|
||||
musicCover.classList.add('hidden');
|
||||
musicCoverFallback.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
if (desktopMusicCover && desktopMusicCoverFallback) {
|
||||
if (currentTrack.coverImageUrl) {
|
||||
desktopMusicCover.setAttribute('src', currentTrack.coverImageUrl);
|
||||
desktopMusicCover.setAttribute('alt', currentTrack.title || 'Music cover');
|
||||
desktopMusicCover.classList.remove('hidden');
|
||||
desktopMusicCoverFallback.classList.add('hidden');
|
||||
} else {
|
||||
desktopMusicCover.setAttribute('src', '');
|
||||
desktopMusicCover.classList.add('hidden');
|
||||
desktopMusicCoverFallback.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function playSong() {
|
||||
if (!playlist.length) {
|
||||
return;
|
||||
}
|
||||
initAudio();
|
||||
if (audio.src !== playlist[currentSongIndex].url) {
|
||||
audio.src = playlist[currentSongIndex].url;
|
||||
@@ -360,9 +523,16 @@ const currentPath = Astro.url.pathname;
|
||||
if (musicPlayIcon) {
|
||||
musicPlayIcon.className = 'fas fa-pause text-xs';
|
||||
}
|
||||
if (desktopMusicPlayIcon) {
|
||||
desktopMusicPlayIcon.className = 'fas fa-pause text-[11px]';
|
||||
}
|
||||
if (musicTitle) {
|
||||
musicTitle.classList.add('text-[var(--primary)]');
|
||||
musicTitle.classList.remove('text-[var(--text-secondary)]');
|
||||
musicTitle.classList.remove('text-[var(--title-color)]');
|
||||
}
|
||||
if (desktopMusicTitle) {
|
||||
desktopMusicTitle.classList.add('text-[var(--primary)]');
|
||||
desktopMusicTitle.classList.remove('text-[var(--title-color)]');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,9 +544,16 @@ const currentPath = Astro.url.pathname;
|
||||
if (musicPlayIcon) {
|
||||
musicPlayIcon.className = 'fas fa-play text-xs';
|
||||
}
|
||||
if (desktopMusicPlayIcon) {
|
||||
desktopMusicPlayIcon.className = 'fas fa-play text-[11px]';
|
||||
}
|
||||
if (musicTitle) {
|
||||
musicTitle.classList.remove('text-[var(--primary)]');
|
||||
musicTitle.classList.add('text-[var(--text-secondary)]');
|
||||
musicTitle.classList.add('text-[var(--title-color)]');
|
||||
}
|
||||
if (desktopMusicTitle) {
|
||||
desktopMusicTitle.classList.remove('text-[var(--primary)]');
|
||||
desktopMusicTitle.classList.add('text-[var(--title-color)]');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +566,9 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
|
||||
function playNext() {
|
||||
if (!playlist.length) {
|
||||
return;
|
||||
}
|
||||
currentSongIndex = (currentSongIndex + 1) % playlist.length;
|
||||
updateTitle();
|
||||
if (isPlaying) {
|
||||
@@ -397,6 +577,9 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
|
||||
function playPrev() {
|
||||
if (!playlist.length) {
|
||||
return;
|
||||
}
|
||||
currentSongIndex = (currentSongIndex - 1 + playlist.length) % playlist.length;
|
||||
updateTitle();
|
||||
if (isPlaying) {
|
||||
@@ -416,8 +599,11 @@ const currentPath = Astro.url.pathname;
|
||||
}
|
||||
|
||||
musicPlay?.addEventListener('click', togglePlay);
|
||||
desktopMusicPlay?.addEventListener('click', togglePlay);
|
||||
musicNext?.addEventListener('click', playNext);
|
||||
desktopMusicNext?.addEventListener('click', playNext);
|
||||
musicPrev?.addEventListener('click', playPrev);
|
||||
desktopMusicPrev?.addEventListener('click', playPrev);
|
||||
musicVolume?.addEventListener('click', toggleMute);
|
||||
|
||||
// Initialize title
|
||||
@@ -438,18 +624,17 @@ const currentPath = Astro.url.pathname;
|
||||
const searchModePanel = document.getElementById('search-mode-panel');
|
||||
const searchModeButtons = Array.from(document.querySelectorAll('.search-mode-btn'));
|
||||
const localeSwitchLinks = Array.from(document.querySelectorAll('[data-locale-switch]'));
|
||||
const searchApiBase = 'http://localhost:5150/api';
|
||||
const searchApiBase = apiBase;
|
||||
const searchInputs = [searchInput, mobileSearchInput].filter(Boolean);
|
||||
const t = window.__termiTranslate;
|
||||
const searchModeConfig = {
|
||||
keyword: {
|
||||
label: 'grep -i',
|
||||
label: t('header.searchPromptKeyword'),
|
||||
hint: t('header.searchHintKeyword'),
|
||||
placeholder: t('header.searchPlaceholderKeyword'),
|
||||
buttonIcon: 'fa-search'
|
||||
},
|
||||
ai: {
|
||||
label: 'ask ai',
|
||||
label: t('header.searchPromptAi'),
|
||||
hint: t('header.searchHintAi'),
|
||||
placeholder: t('header.searchPlaceholderAi'),
|
||||
buttonIcon: 'fa-robot'
|
||||
@@ -771,6 +956,14 @@ const currentPath = Astro.url.pathname;
|
||||
|
||||
syncSearchModeUI();
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
mobileMenu?.classList.add('hidden');
|
||||
mobileMenuBtn?.setAttribute('aria-expanded', 'false');
|
||||
hideSearchResults();
|
||||
}
|
||||
});
|
||||
|
||||
localeSwitchLinks.forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
const nextLocale = link.getAttribute('data-locale-switch');
|
||||
@@ -796,5 +989,15 @@ const currentPath = Astro.url.pathname;
|
||||
) {
|
||||
hideSearchResults();
|
||||
}
|
||||
|
||||
if (
|
||||
mobileMenu &&
|
||||
!mobileMenu.classList.contains('hidden') &&
|
||||
!mobileMenu.contains(target) &&
|
||||
!mobileMenuBtn?.contains(target)
|
||||
) {
|
||||
mobileMenu.classList.add('hidden');
|
||||
mobileMenuBtn?.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user