diff --git a/web/backend/main.py b/web/backend/main.py index 9eff6f2..ef66d89 100644 --- a/web/backend/main.py +++ b/web/backend/main.py @@ -12,6 +12,8 @@ from starlette.middleware.base import BaseHTTPMiddleware from .database import init_db from .routers import auth, users, accounts, login, proxy, cookies +from .schemas import AppInfo +from .version import get_app_version from utils import setup_logger @@ -28,7 +30,7 @@ async def lifespan(app: FastAPI): app = FastAPI( title="斗鱼批量登录后台", - version="1.0.0", + version=get_app_version(), lifespan=lifespan, ) @@ -74,6 +76,11 @@ def health(): return {"status": "ok"} +@app.get("/api/app-info", response_model=AppInfo) +def app_info(): + return AppInfo(version=get_app_version()) + + # ---- 生产环境:serve 前端静态文件 ---- # Docker 部署时前端构建产物会被复制到 web/frontend/dist _FRONTEND_DIST = Path(__file__).resolve().parents[2] / "web" / "frontend" / "dist" diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 80e3cbf..ad0b40c 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -202,6 +202,10 @@ class PlatformInfo(BaseModel): # ---- 通用 ---- +class AppInfo(BaseModel): + version: str + + class MessageResponse(BaseModel): message: str success: bool = True diff --git a/web/backend/version.py b/web/backend/version.py new file mode 100644 index 0000000..97eb68a --- /dev/null +++ b/web/backend/version.py @@ -0,0 +1,23 @@ +"""应用版本信息。""" + +from functools import lru_cache +from importlib.metadata import PackageNotFoundError, version as package_version +from pathlib import Path +import tomllib + + +@lru_cache(maxsize=1) +def get_app_version() -> str: + """从 pyproject.toml 读取应用版本,读取失败时回退到包元数据。""" + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + if pyproject_path.exists(): + with pyproject_path.open("rb") as file: + pyproject = tomllib.load(file) + version = pyproject.get("project", {}).get("version") + if isinstance(version, str) and version: + return version + + try: + return package_version("douyu-login-py") + except PackageNotFoundError: + return "0.0.0" diff --git a/web/frontend/src/api/app.ts b/web/frontend/src/api/app.ts new file mode 100644 index 0000000..a536b94 --- /dev/null +++ b/web/frontend/src/api/app.ts @@ -0,0 +1,6 @@ +import api from './client'; +import type { AppInfo } from './types'; + +export const appApi = { + info: () => api.get('/app-info'), +}; diff --git a/web/frontend/src/api/modules.ts b/web/frontend/src/api/modules.ts index 9a8ac5b..b8b1b07 100644 --- a/web/frontend/src/api/modules.ts +++ b/web/frontend/src/api/modules.ts @@ -1,5 +1,6 @@ export * from './types'; export { accountApi } from './accounts'; +export { appApi } from './app'; export { authApi } from './auth'; export { cookieApi } from './cookies'; export { loginApi } from './login'; diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index fabaff2..de18851 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -13,6 +13,10 @@ export interface MessageDeletedResponse extends MessageResponse { deleted: number; } +export interface AppInfo { + version: string; +} + // ==================== Auth ==================== export interface LoginResult { diff --git a/web/frontend/src/hooks/useAppInfo.ts b/web/frontend/src/hooks/useAppInfo.ts new file mode 100644 index 0000000..05e57ff --- /dev/null +++ b/web/frontend/src/hooks/useAppInfo.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; +import { appApi } from '../api/modules'; +import type { AppInfo } from '../api/modules'; + +export function useAppInfo() { + const [appInfo, setAppInfo] = useState(null); + + useEffect(() => { + let ignore = false; + + appApi.info() + .then((info) => { + if (!ignore) setAppInfo(info); + }) + .catch(() => { + if (!ignore) setAppInfo(null); + }); + + return () => { + ignore = true; + }; + }, []); + + return appInfo; +} diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index a47de8a..9c41f91 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -12,6 +12,7 @@ import { authApi } from '../api/modules'; import { type ThemeMode } from '../store/theme'; import { useTheme } from '../store/useTheme'; import { usePermissions } from '../hooks/usePermissions'; +import { useAppInfo } from '../hooks/useAppInfo'; const { Sider, Content } = Layout; const { Text } = Typography; @@ -35,6 +36,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { const [collapsed, setCollapsed] = useState(false); const { mode, isDark, setMode } = useTheme(); const { can, canAny } = usePermissions(user); + const appInfo = useAppInfo(); useEffect(() => { if (!user) navigate('/login'); @@ -100,6 +102,8 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { const siderTheme = isDark ? 'dark' : 'light'; const siderTextColor = isDark ? '#fff' : undefined; + const secondaryTextColor = isDark ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.45)'; + const versionText = appInfo?.version ? `v${appInfo.version}` : 'v--'; return ( @@ -143,35 +147,58 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { {!collapsed && ( <> {user.username} - + ({ROLE_LABELS[user.role] || user.role}) )} + {!collapsed && ( + + 当前版本 {versionText} + + )} {collapsed ? ( - - ({ key: o.key, label: o.label, icon: o.icon, onClick: () => setMode(o.key) })) }} - trigger={['click']} - > + <> + + ({ key: o.key, label: o.label, icon: o.icon, onClick: () => setMode(o.key) })) }} + trigger={['click']} + > + @@ -192,7 +219,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { size="small" icon={} onClick={handleLogout} - style={{ color: isDark ? 'rgba(255,255,255,0.65)' : undefined }} + style={{ color: isDark ? secondaryTextColor : undefined }} > 退出 diff --git a/web/frontend/src/pages/LoginPage.tsx b/web/frontend/src/pages/LoginPage.tsx index d29f250..5fcf0bd 100644 --- a/web/frontend/src/pages/LoginPage.tsx +++ b/web/frontend/src/pages/LoginPage.tsx @@ -6,13 +6,16 @@ import { authApi } from '../api/modules'; import { setAuth, type AuthUser } from '../store/auth'; import { getErrorMessage } from '../utils/error'; import { useTheme } from '../store/useTheme'; +import { useAppInfo } from '../hooks/useAppInfo'; -const { Title } = Typography; +const { Title, Text } = Typography; export default function LoginPage({ onLogin }: { onLogin?: () => void }) { const [loading, setLoading] = useState(false); const navigate = useNavigate(); const { isDark } = useTheme(); + const appInfo = useAppInfo(); + const versionText = appInfo?.version ? `v${appInfo.version}` : 'v--'; const onFinish = async (values: { username: string; password: string }) => { setLoading(true); @@ -49,6 +52,15 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) { 斗鱼批量登录后台 + + 当前版本 {versionText} +
} placeholder="用户名" />