GAP / app /web /templates /_base.html
misonL's picture
Upload _base.html
53d65a2 verified
Raw
History Blame Contribute Delete
34.5 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Gemini API 代理{% endblock %} - 管理界面</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> {# Flatpickr CSS #}
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script> {# Flatpickr JS #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/zh.js"></script> {# Flatpickr 中文语言包 #}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> {# 全局加载 Chart.js #}
<script>
// 步骤 1: 获取由 url_for 生成的原始 URL 字符串
const rawRoutesFromUrlFor = {
root_get: "/",
login_post: "/login",
// logout_get: "javascript:void(0);", // 登出由JS处理
// manage_page_get: "/manage", // 假设 /manage 重定向到 keys 或其他默认管理页面
manage_context_page: "/manage/context",
get_manage_context_data: "/api/manage/context/data",
update_context_ttl: "/api/manage/context/ttl",
delete_context_entry: "/api/manage/context/delete", // 假设的路径,请根据实际路由调整
manage_keys_page: "/manage/keys",
get_manage_keys_data: "/api/manage/keys/data",
add_api_key: "/api/keys", // 通常 POST 到集合
update_api_key: "/api/keys", // 通常 PUT/PATCH 到集合或特定ID,这里用集合路径示例
delete_api_key: "/api/keys", // 通常 DELETE 到特定ID,这里用集合路径示例
manage_report_page: "/manage/report",
get_report_data: "/api/report/data",
get_api_v2_model_info: "/api/v2/models", // 对应 list_models
swagger_ui_html: "/docs",
redoc_html: "/redoc"
};
console.log("[DEBUG] Raw routes from url_for:", JSON.stringify(rawRoutesFromUrlFor));
// 步骤 2: 处理原始 URL,提取相对路径
window.APP_ROUTES = {};
for (const key in rawRoutesFromUrlFor) {
const originalUrlString = rawRoutesFromUrlFor[key];
try {
if (originalUrlString && typeof originalUrlString === 'string') {
// 使用 window.location.origin 作为基准来正确解析相对路径
const urlObject = new URL(originalUrlString, window.location.origin);
window.APP_ROUTES[key] = urlObject.pathname + urlObject.search + urlObject.hash;
} else {
console.warn("[DEBUG] Invalid originalUrlString for key:", key, "value:", originalUrlString);
window.APP_ROUTES[key] = originalUrlString; // 保留原始值以便调试
}
} catch (e) {
console.error("[DEBUG] Error processing route for key:", key, "value:", originalUrlString, "Error:", e);
window.APP_ROUTES[key] = originalUrlString; // 出错时,保留原始值以便调试
}
}
console.log("[DEBUG] Processed APP_ROUTES by Roo:", JSON.stringify(window.APP_ROUTES));
</script>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
.container { flex-grow: 1; }
footer { background-color: #f8f9fa; padding: 1rem 0; text-align: center; }
header nav a, header nav span { margin-left: 0.5rem; margin-right: 0.5rem; }
/* 新增导航栏优化样式 */
header.bg-primary {
padding-top: 1rem; /* 增加顶部内边距 */
padding-bottom: 1rem; /* 增加底部内边距 */
box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* 添加细微底部阴影 */
}
header h1 {
font-size: 1.75rem; /* 调整标题大小 */
margin-bottom: 0; /* 移除 H1 的默认下边距,以便与导航垂直对齐 */
}
/* GAP 品牌标题样式 */
.brand-title {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 使用一个现代感的字体 */
font-size: 2.2rem; /* 增大字体 */
font-weight: bold; /* 加粗 */
color: #fff; /* 白色字体 */
text-shadow: 1px 1px 2px rgba(0,0,0,0.3), /* 轻微的黑色阴影 */
0 0 10px rgba(255,255,255,0.3); /* 轻微的白色光晕 */
letter-spacing: 1px; /* 增加字间距 */
margin-right: 1rem; /* 与导航链接的间距 */
}
@media (max-width: 768px) {
.brand-title {
font-size: 1.8rem; /* 小屏幕上略微缩小 */
margin-bottom: 0.5rem; /* 与下方导航的间距 */
width: 100%;
text-align: center; /* 小屏幕上居中 */
}
}
#main-nav a.spa-link,
#main-nav span#admin-nav-links-placeholder a, /* 针对动态添加的链接 */
#main-nav a[data-no-spa] {
padding: 0.5rem 0.75rem; /* 为链接添加内边距 */
border-radius: 0.25rem; /* 轻微圆角 */
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; /* 平滑过渡效果 */
text-decoration: none; /* 移除下划线 */
color: #fff; /* 确保默认文字颜色为白色 */
}
#main-nav a.spa-link:hover,
#main-nav span#admin-nav-links-placeholder a:hover,
#main-nav a[data-no-spa]:hover {
background-color: rgba(255, 255, 255, 0.1); /* 悬停时改变背景色 */
color: #fff; /* 确保文字颜色在悬停时仍然清晰 */
}
#main-nav a.spa-link.active,
#main-nav span#admin-nav-links-placeholder a.active { /* 当前激活链接的样式 */
background-color: rgba(255, 255, 255, 0.2);
font-weight: bold; /* 加粗 */
}
/* 登出按钮的特殊样式 */
#logout-button {
color: #ffc107 !important; /* 保持警告色 */
}
#logout-button:hover {
background-color: rgba(255, 193, 7, 0.2) !important; /* 悬停时警告色的浅背景 */
color: #fff !important;
}
@media (max-width: 768px) {
header .container-fluid {
flex-direction: column;
align-items: flex-start;
}
header h1 {
margin-bottom: 0.5rem; /* 小屏幕时标题和导航间加点间距 */
}
header nav {
flex-direction: column;
align-items: flex-start;
width: 100%; /* 导航在小屏幕时占满宽度 */
}
header nav a,
header nav span,
#main-nav span#admin-nav-links-placeholder a { /* 确保动态添加的链接也应用样式 */
margin-left: 0 !important;
margin-right: 0 !important;
margin-top: 0.5rem;
display: block; /* 每个链接占一行 */
width: 100%; /* 链接占满可用宽度 */
padding: 0.5rem 0.75rem; /* 统一内边距 */
}
#main-nav span#admin-nav-links-placeholder {
flex-direction: column; /* 确保动态链接也是垂直排列 */
width: 100%;
}
}
/* 页面主标题样式 - 新方案 V2:更简洁,强调文本本身 */
#main-content-area #page-content-wrapper > h2:first-child,
#main-content-area > .container > h2:first-child,
#main-content-area > h2:first-child {
font-size: 2.25rem; /* 进一步增大字号 */
font-weight: 600; /* 加粗以强调 */
margin-top: 0.5rem; /* 与上方导航栏的间距 */
margin-bottom: 2rem; /* 与下方内容的间距 */
padding-bottom: 0.5rem;
border-bottom: 2px solid #0d6efd; /* 使用主题蓝色作为下划线,更醒目 */
color: #2c3e50; /* 更深的文本颜色,增加对比度 */
line-height: 1.3;
}
/* 移除之前针对 .container.mt-4 > h2:first-child 的特殊边距调整,让上面的规则统一处理 */
</style>
</head>
<body>
<header class="bg-primary text-white p-3 mb-4"> {# p-3 可能会被上面的 padding-top/bottom 覆盖,但保留以防万一 #}
<div class="container-fluid d-flex justify-content-between align-items-center flex-wrap">
<span class="brand-title">GAP</span>
<nav id="main-nav" class="d-flex align-items-center flex-wrap">
<a href="{{ request.app.url_path_for('manage_context_page') }}" class="spa-link">上下文管理</a>
<span id="admin-nav-links-placeholder" class="d-flex">
{# 管理员链接将由JS动态插入 #}
</span>
<a href="{{ request.app.url_path_for('swagger_ui_html') }}" class="ms-md-3" data-no-spa>API文档(Swagger)</a>
<a href="{{ request.app.url_path_for('redoc_html') }}" class="ms-md-3" data-no-spa>API文档(ReDoc)</a>
<a href="#" id="logout-button" class="ms-md-3" data-no-spa>登出</a>
</nav>
</div>
</header>
<main class="container py-4" id="main-content-area">
{% block content %}{% endblock %}
</main>
<footer class="mt-auto">
<p style="display: flex; align-items: center; justify-content: center;">
<span>&copy; Gemini API Proxy.</span>
<a href="https://github.com/MisonL/GAP/" target="_blank" rel="noopener noreferrer" data-no-spa style="display: inline-flex; align-items: center; text-decoration: none; margin-left: 0.5rem;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16" style="margin-right: 0.25rem;">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>GitHub
</a>
</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 登出逻辑
const logoutButton = document.getElementById('logout-button');
if (logoutButton) {
logoutButton.addEventListener('click', function(event) {
event.preventDefault();
localStorage.removeItem('access_token');
window.location.href = window.APP_ROUTES.root_get || "/"; // 使用 APP_ROUTES
});
}
// SPA导航:封装的fetch请求
function updateActiveNavLink(currentPath) {
document.querySelectorAll('#main-nav a.spa-link').forEach(link => {
link.classList.remove('active');
try {
const linkPath = new URL(link.href, window.location.origin).pathname;
const targetPath = new URL(currentPath, window.location.origin).pathname;
if (linkPath === targetPath) {
link.classList.add('active');
}
} catch (e) {
console.error("Error processing link for active state:", link.href, e);
}
});
// 也处理动态添加的管理员链接
document.querySelectorAll('#admin-nav-links-placeholder a.spa-link').forEach(link => {
link.classList.remove('active');
try {
const linkPath = new URL(link.href, window.location.origin).pathname;
const targetPath = new URL(currentPath, window.location.origin).pathname;
if (linkPath === targetPath) {
link.classList.add('active');
}
} catch (e) {
console.error("Error processing admin link for active state:", link.href, e);
}
});
}
async function fetchWithAuth(url, options = {}) {
let pureUrl = url;
try {
const incomingUrl = new URL(url, window.location.origin);
if (incomingUrl.origin === window.location.origin) {
const relativePart = incomingUrl.pathname + incomingUrl.search + incomingUrl.hash;
if (url !== relativePart) {
console.log(`fetchWithAuth: Original url "${url}" was an absolute same-origin URL, purified to "${relativePart}" for internal processing.`);
pureUrl = relativePart; // 使用纯净相对路径进行后续处理
}
}
// 如果 url 是外部 URL(不同源),pureUrl 保持原样,后续逻辑会直接使用它
} catch (e) {
// 如果 url 解析失败 (例如它不是一个有效的绝对URL或相对URL片段,如 "manage_keys_page")
// 假设它是一个打算用作相对路径的名称/片段
console.warn(`fetchWithAuth: Could not parse url "${url}" as a full URL with origin, assuming it's a relative path or name. Error: ${e.message}`);
// pureUrl 保持原样,后续逻辑会尝试用 origin 拼接
}
const token = localStorage.getItem('access_token'); // 使用真实的token
console.log('fetchWithAuth called for original URL:', url, ' (processed as pureUrl:', pureUrl, ') with token:', token ? token.substring(0,10)+'...' : 'null');
let processedUrl = pureUrl; // 从纯净的 URL 或原始外部 URL 开始
// 步骤 1: 如果 pureUrl 是相对路径,则确保它是绝对的
if (pureUrl.startsWith('/') && !pureUrl.startsWith('//')) { // 例如 /manage/keys
processedUrl = window.location.origin + pureUrl;
console.log('fetchWithAuth: Relative pureUrl "', pureUrl, '" converted to absolute:', processedUrl);
} else if (!pureUrl.startsWith('http://') && !pureUrl.startsWith('https://') && !pureUrl.startsWith('//')) {
// 处理非 http/https/相对协议 开头的,且不含冒号的(排除 mailto:, tel: 等)的路径
// 例如 "manage_keys_page"
if (!pureUrl.includes(':')) {
processedUrl = window.location.origin + '/' + pureUrl;
console.log('fetchWithAuth: Non-standard relative pureUrl "', pureUrl, '" converted to absolute:', processedUrl);
} else {
// 如果包含冒号但不是已知协议 (例如 javascript:alert(1)), processedUrl 保持 pureUrl
// 这种情况 fetch 会失败,是预期的
processedUrl = pureUrl;
}
}
// 如果 pureUrl 已经是 'http://', 'https://', or '//' 开头, processedUrl 保持 pureUrl
// 步骤 2: 确保协议是 HTTPS (如果当前页面是 HTTPS 且 processedUrl 是 HTTP)
if (processedUrl.startsWith('http://') && window.location.protocol === 'https:') {
const originalForLog = processedUrl; // 保存原始值用于日志
processedUrl = processedUrl.replace('http://', 'https://');
console.log('fetchWithAuth: Mixed content for URL "', originalForLog, '", changed to HTTPS:', processedUrl);
}
const headers = { ...(options.headers || {}), 'X-SPA-NAV': 'true' };
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const fetchOptions = { ...options, headers: headers };
try {
const response = await fetch(processedUrl, fetchOptions); // 使用 processedUrl
if (response.status === 401) {
console.warn('认证失败 (401),清除token并重定向。');
localStorage.removeItem('access_token');
window.location.href = window.APP_ROUTES.root_get || "/"; // 使用 APP_ROUTES
throw new Error('Unauthorized');
}
return response;
} catch (error) {
console.error('fetchWithAuth 错误:', error);
if (error.message !== 'Unauthorized') throw error; // 如果不是授权错误,则重新抛出
return new Promise(() => {}); // 对于授权错误,返回一个永远不会resolve的Promise以中断后续操作
}
}
// SPA导航:加载页面内容片段
async function loadPageContent(path, pushState = true) {
let pureRelativePathInput = path; // 用于 history.state 的路径
try {
const incomingUrl = new URL(path, window.location.origin);
if (incomingUrl.origin === window.location.origin) {
const relativePart = incomingUrl.pathname + incomingUrl.search + incomingUrl.hash;
if (path !== relativePart) {
console.log(`loadPageContent: Original path "${path}" was an absolute same-origin URL, purified to "${relativePart}" for further processing and history state.`);
pureRelativePathInput = relativePart; // 更新 path 为纯净相对路径
}
}
// 如果 path 是外部 URL,pureRelativePathInput 保持原样,fetchWithAuth 会处理,但 SPA 导航不应到外部
} catch (e) {
console.warn(`loadPageContent: Could not parse path "${path}" as a full URL with origin, assuming it's a relative path or name. Error: ${e.message}`);
// pureRelativePathInput 保持原样
}
const mainContentArea = document.getElementById('main-content-area');
if (!mainContentArea) { console.error('#main-content-area 未找到!'); return; }
mainContentArea.innerHTML = '<p>加载中...</p>';
try {
// fetchWithAuth 将使用 pureRelativePathInput (如果它是相对的) 或原始 path (如果是外部的)
// fetchWithAuth 内部会自己处理到绝对 HTTPS URL 的转换
const response = await fetchWithAuth(pureRelativePathInput);
if (!response.ok) {
mainContentArea.innerHTML = `<div class="alert alert-danger">加载失败: ${response.status} <pre>${await response.text()}</pre></div>`;
return;
}
mainContentArea.innerHTML = await response.text();
const newTitleTag = mainContentArea.querySelector('title');
const pageTitlePart = (newTitleTag && newTitleTag.textContent) ?
newTitleTag.textContent.trim() :
(pureRelativePathInput.substring(pureRelativePathInput.lastIndexOf('/') + 1).replace(/_/g, ' ').replace(/\..+$/, '') || "管理首页"); // 移除了扩展名
document.title = `${pageTitlePart} - 管理界面`;
if (newTitleTag) newTitleTag.remove();
// history.state 中存储纯净的相对路径 (pureRelativePathInput)
// history.pushState 的第三个参数 (URL) 需要是绝对的
if (pureRelativePathInput !== (window.location.pathname + window.location.search + window.location.hash)) {
let historyDisplayUrl = pureRelativePathInput;
// 构建用于浏览器地址栏显示的绝对 URL
if (pureRelativePathInput.startsWith('/') && !pureRelativePathInput.startsWith('//')) {
historyDisplayUrl = window.location.origin + pureRelativePathInput;
} else if (!pureRelativePathInput.startsWith('http://') && !pureRelativePathInput.startsWith('https://') && !pureRelativePathInput.startsWith('//')) {
if (!pureRelativePathInput.includes(':')) {
historyDisplayUrl = window.location.origin + '/' + pureRelativePathInput;
} else {
historyDisplayUrl = pureRelativePathInput; // 如 "mailto:..."
}
}
// 确保显示 URL 是 HTTPS
if (historyDisplayUrl.startsWith('http://') && window.location.protocol === 'https:') {
historyDisplayUrl = historyDisplayUrl.replace('http://', 'https://');
}
// 确保 state 中存储的是 pureRelativePathInput
history[pushState ? 'pushState' : 'replaceState']({ path: pureRelativePathInput }, document.title, historyDisplayUrl);
}
updateActiveNavLink(pureRelativePathInput); // 更新激活的导航链接,使用纯净路径
// 执行新加载内容中的脚本
const scripts = mainContentArea.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
// 复制所有属性 (例如 type, defer, async)
Array.from(oldScript.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
if (oldScript.src) {
newScript.src = oldScript.src; // 如果是外部脚本,设置src
newScript.onload = () => console.log(`SPA: 外部脚本 ${oldScript.src} 已加载并执行。`);
newScript.onerror = () => console.error(`SPA: 外部脚本 ${oldScript.src} 加载失败。`);
} else {
newScript.textContent = oldScript.textContent; // 如果是内联脚本,复制内容
}
// 将新脚本添加到 head 以执行,然后可以考虑移除旧脚本以避免混乱
document.head.appendChild(newScript).parentNode.removeChild(newScript); // 添加并立即移除以执行,或附加到 body
// oldScript.remove(); // 从 mainContentArea 移除原始脚本标签
});
console.log(`SPA: 处理了 ${scripts.length} 个新加载的脚本。`);
} catch (error) {
console.error('loadPageContent 错误:', error);
if (mainContentArea && error.message !== 'Unauthorized') {
mainContentArea.innerHTML = `<div class="alert alert-danger">脚本错误: ${error.message}</div>`;
}
}
}
// SPA导航:处理导航链接点击 (事件委托)
function handleNavLinkClick(event) {
console.log('DEBUG: handleNavLinkClick - Event triggered. Target:', event.target);
const link = event.target.closest('a:not([data-no-spa])');
console.log('DEBUG: handleNavLinkClick - event.target.closest result (link):', link);
if (!link || !link.href) {
console.log('DEBUG: handleNavLinkClick - No valid link or href found, exiting.');
return;
}
console.log('DEBUG: handleNavLinkClick - Link found:', link.href, 'ID:', link.id, 'ClassName:', link.className);
let targetPath;
let isInternalSPAEligible = false;
try {
const parsedUrl = new URL(link.href, window.location.origin); // 解析 link.href
// 检查是否是同源链接
if (parsedUrl.origin === window.location.origin) {
isInternalSPAEligible = true;
targetPath = parsedUrl.pathname + parsedUrl.search + parsedUrl.hash; // 获取相对路径
console.log('DEBUG: handleNavLinkClick - Parsed as internal link. Relative path:', targetPath);
} else {
// 外部链接,不通过 SPA 处理
console.log('DEBUG: handleNavLinkClick - Parsed as external link, letting browser handle:', parsedUrl.href);
return; // 让浏览器执行默认行为
}
} catch (e) {
// 如果 link.href 解析失败 (例如它是一个不完整的片段或无效的URL)
// 这种情况通常不应该发生,因为浏览器在渲染时会尝试将其解析为绝对URL
// 但作为防御性编程,我们记录它并让浏览器默认处理
console.warn('DEBUG: handleNavLinkClick - Error parsing link.href. Href:', link.href, 'Error:', e.message, 'Letting browser handle default.');
return; // 让浏览器执行默认行为
}
// 即使是内部链接,如果是登出按钮,也不通过 SPA 处理
if (link.id === 'logout-button') {
console.log('DEBUG: handleNavLinkClick - Logout button clicked, not handling via SPA.');
return; // 让浏览器(或登出按钮自己的事件监听器)处理
}
if (!isInternalSPAEligible) {
console.log('DEBUG: handleNavLinkClick - Not an internal SPA eligible link, letting browser handle default.');
return; // 理论上这行不会执行,因为上面已经 return 了
}
event.preventDefault(); // 阻止默认的 HTTP 跳转
console.log('DEBUG: handleNavLinkClick - Default action prevented for SPA navigation.');
console.log('DEBUG: handleNavLinkClick - SPA navigation to path:', targetPath);
// 检查是否已经是当前页面 (使用 window.APP_ROUTES.root_get 进行比较)
const rootGetPath = (window.APP_ROUTES && window.APP_ROUTES.root_get) ? window.APP_ROUTES.root_get : "/";
if (targetPath === (window.location.pathname + window.location.search + window.location.hash) && targetPath !== rootGetPath) {
console.log('DEBUG: handleNavLinkClick - Target path is current path and not root, not reloading.');
return;
}
console.log('DEBUG: handleNavLinkClick - Calling loadPageContent for path:', targetPath);
loadPageContent(targetPath, true); // pushState 为 true
}
// 更新导航栏并处理初始页面加载
async function updateAdminNavAndInitialLoad() {
const placeholder = document.getElementById('admin-nav-links-placeholder');
const token = localStorage.getItem('access_token');
if (placeholder) {
placeholder.innerHTML = '';
if (token) {
try {
const contextDataUrl = window.APP_ROUTES.get_manage_context_data || "/manage/context/data"; // 使用 APP_ROUTES
const response = await fetchWithAuth(contextDataUrl);
if (response.ok) {
const data = await response.json();
if (data.is_admin) {
console.log('用户是管理员,添加管理员链接。');
const reportLink = document.createElement('a');
reportLink.href = window.APP_ROUTES.manage_report_page || "/manage/report"; // 使用 APP_ROUTES
reportLink.className = 'text-white ms-md-3 spa-link';
reportLink.textContent = '周期报告';
const keysLink = document.createElement('a');
keysLink.href = window.APP_ROUTES.manage_keys_page || "/manage/keys"; // 使用 APP_ROUTES
keysLink.className = 'text-white ms-md-3 spa-link';
keysLink.textContent = '代理Key管理';
placeholder.appendChild(reportLink);
placeholder.appendChild(keysLink);
} else {
console.log('用户不是管理员,不添加管理员链接。');
}
} else {
console.warn('获取管理员状态失败,状态:', response.status);
}
} catch (e) {
if (e.message !== 'Unauthorized') {
console.error("更新管理员导航时出错:", e);
}
}
} else {
console.log('未找到访问令牌,不添加管理员链接。');
}
}
const currentPath = window.location.pathname + window.location.search + window.location.hash; // 包含查询参数和哈希
console.log('SPA Initial Load: currentPath="', currentPath, '", token="', token ? token.substring(0,10)+'...' : 'null', '"');
if (token) {
let initialLoadPath = currentPath;
const rootPath = window.APP_ROUTES.root_get || "/";
const manageContextPath = window.APP_ROUTES.manage_context_page || "/manage/context";
if (currentPath === rootPath || currentPath === "/") { // 检查是否在根路径
console.log('已登录且在根路径,自动加载默认管理页面。');
initialLoadPath = manageContextPath;
loadPageContent(initialLoadPath, false); // replaceState
} else if (currentPath.startsWith('/manage/')) {
console.log('直接访问管理页面,内容已由服务器渲染,但仍需确保JS初始化和激活链接。');
// initialLoadPath 已经是 currentPath
loadPageContent(initialLoadPath, false); // replaceState
}
// 确保在初始加载后也更新激活链接状态
// 使用 setTimeout 确保在 loadPageContent 完成 DOM 修改后执行
setTimeout(() => updateActiveNavLink(initialLoadPath || window.location.pathname), 0);
} else {
updateActiveNavLink(currentPath); // 未登录时也尝试更新(例如,如果将来有公共SPA页面)
}
}
// Popstate事件监听器
window.addEventListener('popstate', async (event) => {
console.log('popstate 事件触发:', event.state);
const tokenPop = localStorage.getItem('access_token');
const placeholderPop = document.getElementById('admin-nav-links-placeholder');
if (placeholderPop) {
placeholderPop.innerHTML = ''; // 清空以重新填充
if (tokenPop) {
try {
const contextDataUrlPop = window.APP_ROUTES.get_manage_context_data || "/manage/context/data"; // 使用 APP_ROUTES
const response = await fetchWithAuth(contextDataUrlPop);
if (response.ok) {
const data = await response.json();
if (data.is_admin) {
const reportLinkPop = document.createElement('a');
reportLinkPop.href = window.APP_ROUTES.manage_report_page || "/manage/report"; // 使用 APP_ROUTES
reportLinkPop.className = 'text-white ms-md-3 spa-link';
reportLinkPop.textContent = '周期报告';
placeholderPop.appendChild(reportLinkPop);
const keysLinkPop = document.createElement('a');
keysLinkPop.href = window.APP_ROUTES.manage_keys_page || "/manage/keys"; // 使用 APP_ROUTES
keysLinkPop.className = 'text-white ms-md-3 spa-link';
keysLinkPop.textContent = '代理Key管理';
placeholderPop.appendChild(keysLinkPop);
}
}
} catch(e) { if (e.message !== 'Unauthorized') console.error("Popstate: 更新导航栏时出错:", e); }
}
}
const targetPath = (event.state && event.state.path) ? event.state.path : (window.location.pathname + window.location.search + window.location.hash);
console.log('Popstate: 目标路径为', targetPath);
loadPageContent(targetPath, false); // loadPageContent 内部会调用 updateActiveNavLink
});
// DOMContentLoaded时执行初始加载和导航设置
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded: 开始设置。');
updateAdminNavAndInitialLoad();
document.body.addEventListener('click', handleNavLinkClick);
console.log('DOMContentLoaded: SPA click handler attached to body.');
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>