mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-06 16:16:37 +08:00
feat(static): 实现静态资源版本化和模板全局变量支持
- 在Dockerfile中添加默认环境变量配置 - 新增静态资源URL版本化管理功能 - 更新所有模板文件使用static_url函数替代硬编码路径 - 优化错误日志页面移动端按钮布局和响应式设计 - 简化异常处理器返回格式 BREAKING CHANGE: 静态资源URL格式变更,需要重新部署以确保资源正确加载
This commit is contained in:
@@ -8,6 +8,9 @@ COPY ./VERSION /app
|
|||||||
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
COPY ./app /app/app
|
COPY ./app /app/app
|
||||||
|
ENV API_KEYS='["your_api_key_1"]'
|
||||||
|
ENV ALLOWED_TOKENS='["your_token_1"]'
|
||||||
|
ENV TZ='Asia/Shanghai'
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|||||||
@@ -130,11 +130,6 @@ def setup_exception_handlers(app: FastAPI) -> None:
|
|||||||
"""处理通用异常"""
|
"""处理通用异常"""
|
||||||
logger.exception(f"Unhandled Exception: {str(exc)}")
|
logger.exception(f"Unhandled Exception: {str(exc)}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.args[0],
|
status_code=500,
|
||||||
content={
|
content=str(exc),
|
||||||
"error": {
|
|
||||||
"code": exc.args[0],
|
|
||||||
"message": exc.args[1],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ from app.router import (
|
|||||||
)
|
)
|
||||||
from app.service.key.key_manager import get_key_manager_instance
|
from app.service.key.key_manager import get_key_manager_instance
|
||||||
from app.service.stats.stats_service import StatsService
|
from app.service.stats.stats_service import StatsService
|
||||||
|
from app.utils.static_version import get_static_url
|
||||||
|
|
||||||
logger = get_routes_logger()
|
logger = get_routes_logger()
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
# 设置模板全局变量
|
||||||
|
templates.env.globals["static_url"] = get_static_url
|
||||||
|
|
||||||
|
|
||||||
def setup_routers(app: FastAPI) -> None:
|
def setup_routers(app: FastAPI) -> None:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-3xl font-extrabold text-center text-gray-800 mb-8 animate-slide-down">
|
<h2 class="text-3xl font-extrabold text-center text-gray-800 mb-8 animate-slide-down">
|
||||||
<img src="/static/icons/logo.png" alt="Gemini Balance Logo" class="h-9 inline-block align-middle mr-2">
|
<img src="{{ static_url('icons/logo.png') }}" alt="Gemini Balance Logo" class="h-9 inline-block align-middle mr-2">
|
||||||
Gemini Balance
|
Gemini Balance
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,21 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% block title %}Gemini Balance{% endblock %}</title>
|
<title>{% block title %}Gemini Balance{% endblock %}</title>
|
||||||
<link rel="manifest" href="/static/manifest.json" />
|
<link rel="manifest" href="{{ static_url('manifest.json') }}" />
|
||||||
<meta name="theme-color" content="#4F46E5" />
|
<meta name="theme-color" content="#4F46E5" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
<meta name="apple-mobile-web-app-title" content="GBalance" />
|
<meta name="apple-mobile-web-app-title" content="GBalance" />
|
||||||
<link rel="icon" href="/static/icons/icon-192x192.png" />
|
<link rel="icon" href="{{ static_url('icons/icon-192x192.png') }}" />
|
||||||
<link
|
<link
|
||||||
href="/static/css/fonts.css"
|
href="{{ static_url('css/fonts.css') }}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
/>
|
/>
|
||||||
<script src="/static/js/tailwindcss.js"></script>
|
<script src="{{ static_url('js/tailwindcss.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ endblock %} {% block head_extra_styles %}
|
|||||||
|
|
||||||
<h1 class="text-3xl font-extrabold text-center text-gray-800 mb-4">
|
<h1 class="text-3xl font-extrabold text-center text-gray-800 mb-4">
|
||||||
<img
|
<img
|
||||||
src="/static/icons/logo.png"
|
src="{{ static_url('icons/logo.png') }}"
|
||||||
alt="Gemini Balance Logo"
|
alt="Gemini Balance Logo"
|
||||||
class="h-9 inline-block align-middle mr-2"
|
class="h-9 inline-block align-middle mr-2"
|
||||||
/>
|
/>
|
||||||
@@ -2870,7 +2870,7 @@ endblock %} {% block head_extra_styles %}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %} {% block body_scripts %}
|
{% endblock %} {% block body_scripts %}
|
||||||
<script src="/static/js/config_editor.js"></script>
|
<script src="{{ static_url('js/config_editor.js') }}"></script>
|
||||||
<!-- 增强下拉框样式和交互性 -->
|
<!-- 增强下拉框样式和交互性 -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|||||||
@@ -45,6 +45,179 @@ endblock %} {% block head_extra_styles %}
|
|||||||
.search-container {
|
.search-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端主容器布局 */
|
||||||
|
.mobile-buttons-container {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 1rem !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端搜索控件布局优化 */
|
||||||
|
.mobile-search-controls {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
gap: 0.75rem !important;
|
||||||
|
width: 100% !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮容器在移动端的布局 */
|
||||||
|
.buttons-container-responsive {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
width: 100% !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
justify-content: stretch !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端所有按钮样式 */
|
||||||
|
.buttons-container-responsive button {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
text-align: center !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中等屏幕优化 */
|
||||||
|
@media (max-width: 1024px) and (min-width: 769px) {
|
||||||
|
.buttons-container-responsive {
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-container-responsive button {
|
||||||
|
flex-shrink: 1 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
padding-left: 0.75rem !important;
|
||||||
|
padding-right: 0.75rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏幕(手机)特殊优化 - 确保按钮在边框内 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* 强制重写主容器布局 */
|
||||||
|
.mobile-buttons-container {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
gap: 1rem !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域在移动端占满宽度 */
|
||||||
|
.mobile-search-controls {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮区域完全重新布局 */
|
||||||
|
.buttons-container-responsive {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 所有按钮统一样式 */
|
||||||
|
.buttons-container-responsive button {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
line-height: 1.25rem !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特别针对清空全部按钮 */
|
||||||
|
#deleteAllLogsBtn {
|
||||||
|
background-color: #f87171 !important;
|
||||||
|
border: 1px solid #f87171 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#deleteAllLogsBtn:hover {
|
||||||
|
background-color: #ef4444 !important;
|
||||||
|
border: 1px solid #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保容器不会溢出父级 */
|
||||||
|
.mobile-buttons-container,
|
||||||
|
.mobile-buttons-container > *,
|
||||||
|
.buttons-container-responsive,
|
||||||
|
.buttons-container-responsive > * {
|
||||||
|
max-width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 额外的安全边距控制 */
|
||||||
|
.mobile-buttons-container .grid {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保主内容区域有适当的内边距 */
|
||||||
|
.rounded-xl.p-6 {
|
||||||
|
padding-left: 1rem !important;
|
||||||
|
padding-right: 1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕额外优化 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.mobile-buttons-container {
|
||||||
|
gap: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-container-responsive {
|
||||||
|
gap: 0.4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-container-responsive button {
|
||||||
|
padding: 0.4rem 0.8rem !important;
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主容器内边距进一步缩小 */
|
||||||
|
.rounded-xl.p-6 {
|
||||||
|
padding-left: 0.75rem !important;
|
||||||
|
padding-right: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保清空全部按钮文字不会太挤 */
|
||||||
|
#deleteAllLogsBtn i {
|
||||||
|
margin-right: 0.25rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
@@ -586,7 +759,7 @@ endblock %} {% block head_extra_styles %}
|
|||||||
class="text-3xl font-extrabold text-center text-gray-800 mb-4"
|
class="text-3xl font-extrabold text-center text-gray-800 mb-4"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/static/icons/logo.png"
|
src="{{ static_url('icons/logo.png') }}"
|
||||||
alt="Gemini Balance Logo"
|
alt="Gemini Balance Logo"
|
||||||
class="h-9 inline-block align-middle mr-2"
|
class="h-9 inline-block align-middle mr-2"
|
||||||
/>
|
/>
|
||||||
@@ -636,10 +809,10 @@ endblock %} {% block head_extra_styles %}
|
|||||||
|
|
||||||
<!-- 搜索与操作控件 -->
|
<!-- 搜索与操作控件 -->
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 lg:grid-cols-[1fr_auto] items-center gap-4 mb-6"
|
class="grid grid-cols-1 lg:grid-cols-[1fr_auto] items-center gap-4 mb-6 mobile-buttons-container"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full"
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full mobile-search-controls"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -684,7 +857,7 @@ endblock %} {% block head_extra_styles %}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 flex-shrink-0">
|
<div class="flex items-center gap-3 flex-shrink-0 buttons-container-responsive">
|
||||||
<button
|
<button
|
||||||
id="searchBtn"
|
id="searchBtn"
|
||||||
class="flex items-center justify-center px-4 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md whitespace-nowrap"
|
class="flex items-center justify-center px-4 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md whitespace-nowrap"
|
||||||
@@ -1041,7 +1214,7 @@ endblock %} {% block head_extra_styles %}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block body_scripts %}
|
{% endblock %} {% block body_scripts %}
|
||||||
<script src="/static/js/error_logs.js"></script>
|
<script src="{{ static_url('js/error_logs.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
// error_logs.html specific JS initialization (if any)
|
// error_logs.html specific JS initialization (if any)
|
||||||
// e.g., initialize date pickers or other elements if needed
|
// e.g., initialize date pickers or other elements if needed
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
静态资源版本控制工具
|
||||||
|
用于给CSS和JS文件添加版本参数,避免浏览器缓存问题
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from app.utils.helpers import get_current_version
|
||||||
|
|
||||||
|
|
||||||
|
class StaticVersionManager:
|
||||||
|
"""静态资源版本管理器"""
|
||||||
|
|
||||||
|
def __init__(self, static_dir: str = "app/static"):
|
||||||
|
self.static_dir = Path(static_dir)
|
||||||
|
self._version_cache: Dict[str, str] = {}
|
||||||
|
self._use_file_hash = True # 是否使用文件哈希作为版本号
|
||||||
|
|
||||||
|
def get_version_for_file(self, file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
获取文件的版本号
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 相对于static目录的文件路径,如 'css/fonts.css'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
版本号字符串
|
||||||
|
"""
|
||||||
|
if self._use_file_hash:
|
||||||
|
return self._get_file_hash_version(file_path)
|
||||||
|
else:
|
||||||
|
return self._get_app_version()
|
||||||
|
|
||||||
|
def _get_file_hash_version(self, file_path: str) -> str:
|
||||||
|
"""基于文件内容生成哈希版本号"""
|
||||||
|
# 如果已经缓存过,直接返回
|
||||||
|
if file_path in self._version_cache:
|
||||||
|
return self._version_cache[file_path]
|
||||||
|
|
||||||
|
full_path = self.static_dir / file_path
|
||||||
|
|
||||||
|
if not full_path.exists():
|
||||||
|
# 文件不存在,使用应用版本号作为fallback
|
||||||
|
version = self._get_app_version()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# 读取文件内容并计算MD5哈希
|
||||||
|
with open(full_path, "rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
hash_object = hashlib.md5(content)
|
||||||
|
version = hash_object.hexdigest()[:8] # 取前8位
|
||||||
|
except Exception:
|
||||||
|
# 读取失败,使用应用版本号作为fallback
|
||||||
|
version = self._get_app_version()
|
||||||
|
|
||||||
|
# 缓存结果
|
||||||
|
self._version_cache[file_path] = version
|
||||||
|
return version
|
||||||
|
|
||||||
|
def _get_app_version(self) -> str:
|
||||||
|
"""获取应用程序版本号"""
|
||||||
|
try:
|
||||||
|
return get_current_version().replace(".", "")
|
||||||
|
except Exception:
|
||||||
|
# 如果获取版本失败,使用时间戳
|
||||||
|
return str(int(time.time()))
|
||||||
|
|
||||||
|
def get_versioned_url(self, file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
获取带版本参数的URL
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 相对于static目录的文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
带版本参数的URL
|
||||||
|
"""
|
||||||
|
version = self.get_version_for_file(file_path)
|
||||||
|
return f"/static/{file_path}?v={version}"
|
||||||
|
|
||||||
|
def clear_cache(self):
|
||||||
|
"""清空版本缓存"""
|
||||||
|
self._version_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# 全局实例
|
||||||
|
_static_version_manager = StaticVersionManager()
|
||||||
|
|
||||||
|
|
||||||
|
def get_static_url(file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
获取静态资源的版本化URL
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 相对于static目录的文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
带版本参数的完整URL
|
||||||
|
|
||||||
|
Example:
|
||||||
|
get_static_url('css/fonts.css') -> '/static/css/fonts.css?v=a1b2c3d4'
|
||||||
|
get_static_url('js/config_editor.js') -> '/static/js/config_editor.js?v=e5f6g7h8'
|
||||||
|
"""
|
||||||
|
return _static_version_manager.get_versioned_url(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_static_cache():
|
||||||
|
"""清空静态资源版本缓存"""
|
||||||
|
_static_version_manager.clear_cache()
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=128)
|
||||||
|
def get_cached_static_url(file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
获取缓存的静态资源URL(用于开发环境)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 相对于static目录的文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
带版本参数的完整URL
|
||||||
|
"""
|
||||||
|
return get_static_url(file_path)
|
||||||
Reference in New Issue
Block a user