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:
2026-03-29 21:36:13 +08:00
parent 84f82c2a7e
commit 92a85eef20
137 changed files with 14181 additions and 2691 deletions

View File

@@ -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>