285 lines
12 KiB
HTML
285 lines
12 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="color-scheme" content="light dark">
|
|
<title>{{ title }} · {{ config["PROJECT_TITLE"] }}</title>
|
|
<script>
|
|
(() => {
|
|
const storageKey = 'ksp-theme';
|
|
const cookieKey = 'ksp_theme';
|
|
const readSavedTheme = () => {
|
|
try {
|
|
const savedTheme = localStorage.getItem(storageKey);
|
|
if (savedTheme === 'dark' || savedTheme === 'light') {
|
|
return savedTheme;
|
|
}
|
|
} catch (error) {
|
|
// Ignore storage access failures and fall back to cookies/system preference.
|
|
}
|
|
|
|
const cookieMatch = document.cookie.match(/(?:^|; )ksp_theme=(dark|light)(?:;|$)/);
|
|
return cookieMatch ? cookieMatch[1] : null;
|
|
};
|
|
const persistTheme = (theme) => {
|
|
try {
|
|
localStorage.setItem(storageKey, theme);
|
|
} catch (error) {
|
|
// Ignore storage access failures and rely on cookies for persistence.
|
|
}
|
|
|
|
document.cookie = `${cookieKey}=${theme}; path=/; max-age=31536000; SameSite=Lax`;
|
|
};
|
|
const applyDocumentTheme = (theme) => {
|
|
document.documentElement.dataset.theme = theme;
|
|
document.documentElement.style.colorScheme = theme;
|
|
};
|
|
const savedTheme = readSavedTheme();
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
const resolvedTheme = savedTheme === 'dark' || savedTheme === 'light'
|
|
? savedTheme
|
|
: (prefersDark ? 'dark' : 'light');
|
|
|
|
applyDocumentTheme(resolvedTheme);
|
|
window.__kspTheme = {
|
|
storageKey,
|
|
readSavedTheme,
|
|
persistTheme,
|
|
applyDocumentTheme,
|
|
};
|
|
})();
|
|
</script>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
|
</head>
|
|
<body>
|
|
<div class="page-shell">
|
|
<header class="site-header">
|
|
<a class="brand" href="{{ url_for('web.dashboard') }}">
|
|
<span class="brand-name">KSP Operation Hangar</span>
|
|
</a>
|
|
<div class="site-header-tools">
|
|
<nav class="site-nav">
|
|
<a href="{{ url_for('web.dashboard') }}">控制台</a>
|
|
<a href="{{ url_for('web.engine_list') }}">引擎目录</a>
|
|
<a href="{{ url_for('web.communication_list') }}">通信部件</a>
|
|
<a href="{{ url_for('web.tank_list') }}">燃料箱</a>
|
|
<a href="{{ url_for('web.vehicle_list') }}">载具成本</a>
|
|
<a href="{{ url_for('web.mission_preview_home') }}">任务运营</a>
|
|
<a href="{{ url_for('web.fuel_converter') }}">燃料换算</a>
|
|
<a href="{{ url_for('api.health') }}">API 健康检查</a>
|
|
</nav>
|
|
<button class="theme-toggle" type="button" data-theme-toggle aria-label="切换深浅色模式" aria-pressed="false">
|
|
<span class="theme-toggle-track" aria-hidden="true">
|
|
<span class="theme-toggle-thumb"></span>
|
|
</span>
|
|
<span class="theme-toggle-copy">
|
|
<span class="theme-toggle-label">主题</span>
|
|
<span class="theme-toggle-value" data-theme-toggle-label>浅色</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<div class="flash-stack">
|
|
{% for category, message in messages %}
|
|
<div class="flash-message {{ category }}">{{ message }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
(() => {
|
|
const themeManager = window.__kspTheme || null;
|
|
const themeStorageKey = themeManager?.storageKey || 'ksp-theme';
|
|
const themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
const resolveTheme = () => {
|
|
const savedTheme = themeManager?.readSavedTheme?.();
|
|
if (savedTheme === 'dark' || savedTheme === 'light') {
|
|
return savedTheme;
|
|
}
|
|
return themeMediaQuery.matches ? 'dark' : 'light';
|
|
};
|
|
|
|
const applyTheme = (theme) => {
|
|
const resolvedTheme = theme === 'dark' ? 'dark' : 'light';
|
|
if (themeManager?.applyDocumentTheme) {
|
|
themeManager.applyDocumentTheme(resolvedTheme);
|
|
} else {
|
|
document.documentElement.dataset.theme = resolvedTheme;
|
|
document.documentElement.style.colorScheme = resolvedTheme;
|
|
}
|
|
|
|
for (const button of document.querySelectorAll('[data-theme-toggle]')) {
|
|
button.setAttribute('aria-pressed', String(resolvedTheme === 'dark'));
|
|
}
|
|
|
|
for (const label of document.querySelectorAll('[data-theme-toggle-label]')) {
|
|
label.textContent = resolvedTheme === 'dark' ? '深色' : '浅色';
|
|
}
|
|
};
|
|
|
|
const bindThemeToggle = (root = document) => {
|
|
for (const button of root.querySelectorAll('[data-theme-toggle]')) {
|
|
button.onclick = () => {
|
|
const nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
if (themeManager?.persistTheme) {
|
|
themeManager.persistTheme(nextTheme);
|
|
} else {
|
|
localStorage.setItem(themeStorageKey, nextTheme);
|
|
}
|
|
applyTheme(nextTheme);
|
|
};
|
|
}
|
|
};
|
|
|
|
const formatDisplayDatetime = (isoValue) => {
|
|
if (!isoValue) {
|
|
return '';
|
|
}
|
|
return isoValue.slice(0, 16).replace(/-/g, '/').replace('T', ' ');
|
|
};
|
|
|
|
const parseDisplayDatetime = (rawValue) => {
|
|
const cleaned = (rawValue || '').trim();
|
|
if (!cleaned) {
|
|
return '';
|
|
}
|
|
|
|
const normalized = cleaned
|
|
.replace(/[年.]/g, '/')
|
|
.replace(/月/g, '/')
|
|
.replace(/日/g, ' ')
|
|
.replace(/T/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
const match = normalized.match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})\s+(\d{1,2}):(\d{1,2})$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const [, yearValue, monthValue, dayValue, hourValue, minuteValue] = match;
|
|
const year = Number(yearValue);
|
|
const month = Number(monthValue);
|
|
const day = Number(dayValue);
|
|
const hour = Number(hourValue);
|
|
const minute = Number(minuteValue);
|
|
const isoValue = `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}T${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
const parsedDate = new Date(`${isoValue}:00`);
|
|
if (Number.isNaN(parsedDate.getTime())) {
|
|
return null;
|
|
}
|
|
if (
|
|
parsedDate.getFullYear() !== year
|
|
|| parsedDate.getMonth() + 1 !== month
|
|
|| parsedDate.getDate() !== day
|
|
|| parsedDate.getHours() !== hour
|
|
|| parsedDate.getMinutes() !== minute
|
|
) {
|
|
return null;
|
|
}
|
|
return isoValue;
|
|
};
|
|
|
|
const bindDatetimePickerTriggers = (root = document) => {
|
|
const controls = root.querySelectorAll('[data-datetime-control]');
|
|
for (const control of controls) {
|
|
if (control.dataset.datetimeControlBound === 'true') {
|
|
continue;
|
|
}
|
|
control.dataset.datetimeControlBound = 'true';
|
|
|
|
const displayInput = control.querySelector('.mission-datetime-display-input');
|
|
const button = control.querySelector('[data-datetime-picker-target]');
|
|
if (!displayInput || !button) {
|
|
continue;
|
|
}
|
|
|
|
const pickerInput = document.getElementById(button.dataset.datetimePickerTarget || '');
|
|
if (!pickerInput) {
|
|
continue;
|
|
}
|
|
|
|
const syncPickerFromDisplay = () => {
|
|
const parsedValue = parseDisplayDatetime(displayInput.value);
|
|
if (parsedValue === '') {
|
|
pickerInput.value = '';
|
|
return '';
|
|
}
|
|
if (parsedValue === null) {
|
|
return null;
|
|
}
|
|
pickerInput.value = parsedValue;
|
|
displayInput.value = formatDisplayDatetime(parsedValue);
|
|
return parsedValue;
|
|
};
|
|
|
|
button.addEventListener('click', () => {
|
|
syncPickerFromDisplay();
|
|
if (typeof pickerInput.showPicker === 'function') {
|
|
pickerInput.showPicker();
|
|
return;
|
|
}
|
|
pickerInput.focus();
|
|
pickerInput.click();
|
|
});
|
|
|
|
pickerInput.addEventListener('change', () => {
|
|
displayInput.value = formatDisplayDatetime(pickerInput.value);
|
|
});
|
|
|
|
displayInput.addEventListener('blur', () => {
|
|
syncPickerFromDisplay();
|
|
});
|
|
|
|
displayInput.addEventListener('input', () => {
|
|
if (!displayInput.value.trim()) {
|
|
pickerInput.value = '';
|
|
}
|
|
});
|
|
|
|
if (pickerInput.value && !displayInput.value.trim()) {
|
|
displayInput.value = formatDisplayDatetime(pickerInput.value);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.bindDatetimePickerTriggers = bindDatetimePickerTriggers;
|
|
window.bindThemeToggle = bindThemeToggle;
|
|
bindThemeToggle();
|
|
applyTheme(resolveTheme());
|
|
|
|
const handleThemeMediaChange = (event) => {
|
|
if (localStorage.getItem(themeStorageKey)) {
|
|
return;
|
|
}
|
|
applyTheme(event.matches ? 'dark' : 'light');
|
|
};
|
|
|
|
if (typeof themeMediaQuery.addEventListener === 'function') {
|
|
themeMediaQuery.addEventListener('change', handleThemeMediaChange);
|
|
} else if (typeof themeMediaQuery.addListener === 'function') {
|
|
themeMediaQuery.addListener(handleThemeMediaChange);
|
|
}
|
|
|
|
window.addEventListener('storage', (event) => {
|
|
if (event.key && event.key !== themeStorageKey) {
|
|
return;
|
|
}
|
|
applyTheme(resolveTheme());
|
|
});
|
|
|
|
bindDatetimePickerTriggers();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|