增加了版本号系统
This commit is contained in:
+8
-1
@@ -12,6 +12,8 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
|
|
||||||
from .database import init_db
|
from .database import init_db
|
||||||
from .routers import auth, users, accounts, login, proxy, cookies
|
from .routers import auth, users, accounts, login, proxy, cookies
|
||||||
|
from .schemas import AppInfo
|
||||||
|
from .version import get_app_version
|
||||||
from utils import setup_logger
|
from utils import setup_logger
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="斗鱼批量登录后台",
|
title="斗鱼批量登录后台",
|
||||||
version="1.0.0",
|
version=get_app_version(),
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,6 +76,11 @@ def health():
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/app-info", response_model=AppInfo)
|
||||||
|
def app_info():
|
||||||
|
return AppInfo(version=get_app_version())
|
||||||
|
|
||||||
|
|
||||||
# ---- 生产环境:serve 前端静态文件 ----
|
# ---- 生产环境:serve 前端静态文件 ----
|
||||||
# Docker 部署时前端构建产物会被复制到 web/frontend/dist
|
# Docker 部署时前端构建产物会被复制到 web/frontend/dist
|
||||||
_FRONTEND_DIST = Path(__file__).resolve().parents[2] / "web" / "frontend" / "dist"
|
_FRONTEND_DIST = Path(__file__).resolve().parents[2] / "web" / "frontend" / "dist"
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ class PlatformInfo(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# ---- 通用 ----
|
# ---- 通用 ----
|
||||||
|
class AppInfo(BaseModel):
|
||||||
|
version: str
|
||||||
|
|
||||||
|
|
||||||
class MessageResponse(BaseModel):
|
class MessageResponse(BaseModel):
|
||||||
message: str
|
message: str
|
||||||
success: bool = True
|
success: bool = True
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import api from './client';
|
||||||
|
import type { AppInfo } from './types';
|
||||||
|
|
||||||
|
export const appApi = {
|
||||||
|
info: () => api.get<AppInfo, AppInfo>('/app-info'),
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export { accountApi } from './accounts';
|
export { accountApi } from './accounts';
|
||||||
|
export { appApi } from './app';
|
||||||
export { authApi } from './auth';
|
export { authApi } from './auth';
|
||||||
export { cookieApi } from './cookies';
|
export { cookieApi } from './cookies';
|
||||||
export { loginApi } from './login';
|
export { loginApi } from './login';
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export interface MessageDeletedResponse extends MessageResponse {
|
|||||||
deleted: number;
|
deleted: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppInfo {
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Auth ====================
|
// ==================== Auth ====================
|
||||||
|
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
|
|||||||
@@ -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<AppInfo | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let ignore = false;
|
||||||
|
|
||||||
|
appApi.info()
|
||||||
|
.then((info) => {
|
||||||
|
if (!ignore) setAppInfo(info);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!ignore) setAppInfo(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ignore = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return appInfo;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { authApi } from '../api/modules';
|
|||||||
import { type ThemeMode } from '../store/theme';
|
import { type ThemeMode } from '../store/theme';
|
||||||
import { useTheme } from '../store/useTheme';
|
import { useTheme } from '../store/useTheme';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
|
import { useAppInfo } from '../hooks/useAppInfo';
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
const { Sider, Content } = Layout;
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -35,6 +36,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const { mode, isDark, setMode } = useTheme();
|
const { mode, isDark, setMode } = useTheme();
|
||||||
const { can, canAny } = usePermissions(user);
|
const { can, canAny } = usePermissions(user);
|
||||||
|
const appInfo = useAppInfo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) navigate('/login');
|
if (!user) navigate('/login');
|
||||||
@@ -100,6 +102,8 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
|
|
||||||
const siderTheme = isDark ? 'dark' : 'light';
|
const siderTheme = isDark ? 'dark' : 'light';
|
||||||
const siderTextColor = isDark ? '#fff' : undefined;
|
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 (
|
return (
|
||||||
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
||||||
@@ -143,35 +147,58 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<>
|
<>
|
||||||
<Text style={{ color: siderTextColor }}>{user.username}</Text>
|
<Text style={{ color: siderTextColor }}>{user.username}</Text>
|
||||||
<Text style={{ color: isDark ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.45)', fontSize: 12 }}>
|
<Text style={{ color: secondaryTextColor, fontSize: 12 }}>
|
||||||
({ROLE_LABELS[user.role] || user.role})
|
({ROLE_LABELS[user.role] || user.role})
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
{!collapsed && (
|
||||||
|
<Text style={{
|
||||||
|
display: 'block',
|
||||||
|
color: secondaryTextColor,
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}>
|
||||||
|
当前版本 {versionText}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<>
|
||||||
<Dropdown
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
menu={{ items: THEME_OPTIONS.map(o => ({ key: o.key, label: o.label, icon: o.icon, onClick: () => setMode(o.key) })) }}
|
<Dropdown
|
||||||
trigger={['click']}
|
menu={{ items: THEME_OPTIONS.map(o => ({ key: o.key, label: o.label, icon: o.icon, onClick: () => setMode(o.key) })) }}
|
||||||
>
|
trigger={['click']}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={isDark ? <MoonOutlined /> : <SunOutlined />}
|
||||||
|
style={{ color: secondaryTextColor }}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={isDark ? <MoonOutlined /> : <SunOutlined />}
|
icon={<LogoutOutlined />}
|
||||||
style={{ color: isDark ? 'rgba(255,255,255,0.65)' : undefined }}
|
onClick={handleLogout}
|
||||||
|
style={{ color: secondaryTextColor }}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Space>
|
||||||
<Button
|
<Text style={{
|
||||||
block
|
display: 'block',
|
||||||
type="text"
|
color: secondaryTextColor,
|
||||||
size="small"
|
fontSize: 11,
|
||||||
icon={<LogoutOutlined />}
|
lineHeight: '18px',
|
||||||
onClick={handleLogout}
|
textAlign: 'center',
|
||||||
style={{ color: isDark ? 'rgba(255,255,255,0.65)' : undefined }}
|
marginTop: 8,
|
||||||
/>
|
}}>
|
||||||
</Space>
|
{versionText}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -182,7 +209,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={isDark ? <MoonOutlined /> : <SunOutlined />}
|
icon={isDark ? <MoonOutlined /> : <SunOutlined />}
|
||||||
style={{ color: isDark ? 'rgba(255,255,255,0.65)' : undefined }}
|
style={{ color: isDark ? secondaryTextColor : undefined }}
|
||||||
>
|
>
|
||||||
{THEME_OPTIONS.find(o => o.key === mode)?.label}
|
{THEME_OPTIONS.find(o => o.key === mode)?.label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -192,7 +219,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
size="small"
|
size="small"
|
||||||
icon={<LogoutOutlined />}
|
icon={<LogoutOutlined />}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
style={{ color: isDark ? 'rgba(255,255,255,0.65)' : undefined }}
|
style={{ color: isDark ? secondaryTextColor : undefined }}
|
||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import { authApi } from '../api/modules';
|
|||||||
import { setAuth, type AuthUser } from '../store/auth';
|
import { setAuth, type AuthUser } from '../store/auth';
|
||||||
import { getErrorMessage } from '../utils/error';
|
import { getErrorMessage } from '../utils/error';
|
||||||
import { useTheme } from '../store/useTheme';
|
import { useTheme } from '../store/useTheme';
|
||||||
|
import { useAppInfo } from '../hooks/useAppInfo';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
export default function LoginPage({ onLogin }: { onLogin?: () => void }) {
|
export default function LoginPage({ onLogin }: { onLogin?: () => void }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
|
const appInfo = useAppInfo();
|
||||||
|
const versionText = appInfo?.version ? `v${appInfo.version}` : 'v--';
|
||||||
|
|
||||||
const onFinish = async (values: { username: string; password: string }) => {
|
const onFinish = async (values: { username: string; password: string }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -49,6 +52,15 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) {
|
|||||||
<Title level={3} style={{ textAlign: 'center', marginBottom: 32 }}>
|
<Title level={3} style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||||
斗鱼批量登录后台
|
斗鱼批量登录后台
|
||||||
</Title>
|
</Title>
|
||||||
|
<Text type="secondary" style={{
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: -20,
|
||||||
|
marginBottom: 24,
|
||||||
|
fontSize: 12,
|
||||||
|
}}>
|
||||||
|
当前版本 {versionText}
|
||||||
|
</Text>
|
||||||
<Form onFinish={onFinish} size="large">
|
<Form onFinish={onFinish} size="large">
|
||||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||||
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
||||||
|
|||||||
Reference in New Issue
Block a user