mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-04 23:29:00 +08:00
feat: Add the ability to get the latest version
This commit is contained in:
+36
-3
@@ -1,9 +1,10 @@
|
|||||||
|
import httpx
|
||||||
|
import time
|
||||||
from fastapi import APIRouter, Depends, Form
|
from fastapi import APIRouter, Depends, Form
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from services.config import ConfigCenter
|
from services.config import ConfigCenter
|
||||||
from services.auth import get_current_active_user, User, has_users
|
from services.auth import get_current_active_user, User, has_users
|
||||||
from api.response import success
|
from api.response import success
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||||
|
|
||||||
|
|
||||||
@@ -37,9 +38,41 @@ async def get_all_config(
|
|||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def get_system_status():
|
async def get_system_status():
|
||||||
system_info = {
|
system_info = {
|
||||||
"version": "1.0.0",
|
"version": "v1.0.0",
|
||||||
"title": await ConfigCenter.get("APP_NAME", "Foxel"),
|
"title": await ConfigCenter.get("APP_NAME", "Foxel"),
|
||||||
"logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"),
|
"logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"),
|
||||||
"is_initialized": await has_users()
|
"is_initialized": await has_users()
|
||||||
}
|
}
|
||||||
return success(system_info)
|
return success(system_info)
|
||||||
|
|
||||||
|
|
||||||
|
latest_version_cache = {
|
||||||
|
"timestamp": 0,
|
||||||
|
"data": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/latest-version")
|
||||||
|
async def get_latest_version():
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - latest_version_cache["timestamp"] < 3600 and latest_version_cache["data"]:
|
||||||
|
return success(latest_version_cache["data"])
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0, proxy="http://127.0.0.1:7897") as client:
|
||||||
|
resp = await client.get(
|
||||||
|
"https://api.github.com/repos/DrizzleTime/Foxel/releases/latest",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
version_info = {
|
||||||
|
"latest_version": data.get("tag_name"),
|
||||||
|
"body": data.get("body")
|
||||||
|
}
|
||||||
|
latest_version_cache["timestamp"] = current_time
|
||||||
|
latest_version_cache["data"] = version_info
|
||||||
|
return success(version_info)
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
if latest_version_cache["data"]:
|
||||||
|
return success(latest_version_cache["data"])
|
||||||
|
return success({"latest_version": None, "body": None})
|
||||||
|
|||||||
+1
-1
@@ -28,4 +28,4 @@ class AutomationTaskRead(AutomationTaskBase):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|||||||
try:
|
try:
|
||||||
if token_str and token_str.startswith("Bearer "):
|
if token_str and token_str.startswith("Bearer "):
|
||||||
token = token_str.split(" ")[1]
|
token = token_str.split(" ")[1]
|
||||||
payload = jwt.decode(token, ConfigCenter.get_secret_key("SECRET_KEY"), algorithms=[ALGORITHM])
|
payload = jwt.decode(token, await ConfigCenter.get_secret_key("SECRET_KEY"), algorithms=[ALGORITHM])
|
||||||
username = payload.get("sub")
|
username = payload.get("sub")
|
||||||
if username:
|
if username:
|
||||||
user_account = await UserAccount.get_or_none(username=username)
|
user_account = await UserAccount.get_or_none(username=username)
|
||||||
|
|||||||
+4
-1
@@ -11,6 +11,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.8.0",
|
"react-router": "^7.8.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -751,7 +752,7 @@
|
|||||||
|
|
||||||
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
"react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="],
|
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||||
|
|
||||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|
||||||
@@ -899,6 +900,8 @@
|
|||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
|
"@uiw/react-markdown-preview/react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="],
|
||||||
|
|
||||||
"@uiw/react-markdown-preview/rehype-prism-plus": ["rehype-prism-plus@2.0.0", "", { "dependencies": { "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "refractor": "^4.8.0", "rehype-parse": "^9.0.0", "unist-util-filter": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ=="],
|
"@uiw/react-markdown-preview/rehype-prism-plus": ["rehype-prism-plus@2.0.0", "", { "dependencies": { "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "refractor": "^4.8.0", "rehype-parse": "^9.0.0", "unist-util-filter": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.8.0"
|
"react-router": "^7.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -25,3 +25,7 @@ export interface SystemStatus {
|
|||||||
export async function status() {
|
export async function status() {
|
||||||
return request<SystemStatus>('/config/status');
|
return request<SystemStatus>('/config/status');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLatestVersion() {
|
||||||
|
return request<{ latest_version: string | null, body: string | null }>('/config/latest-version');
|
||||||
|
}
|
||||||
|
|||||||
+112
-23
@@ -1,11 +1,19 @@
|
|||||||
import { Layout, Menu, theme, Button, Modal } from 'antd';
|
import { Layout, Menu, theme, Button, Modal, Tag, Tooltip } from 'antd';
|
||||||
import { navGroups } from './nav.ts';
|
import { navGroups } from './nav.ts';
|
||||||
import type { NavItem, NavGroup } from './nav.ts';
|
import type { NavItem, NavGroup } from './nav.ts';
|
||||||
import { memo, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { useSystemStatus } from '../contexts/SystemContext.tsx';
|
import { useSystemStatus } from '../contexts/SystemContext.tsx';
|
||||||
import { GithubOutlined, MenuFoldOutlined, SendOutlined, WechatOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
GithubOutlined,
|
||||||
|
MenuFoldOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
WechatOutlined,
|
||||||
|
WarningOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
import '../styles/sider-menu.css';
|
import '../styles/sider-menu.css';
|
||||||
|
import { getLatestVersion } from '../api/config.ts';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
export interface SideNavProps {
|
export interface SideNavProps {
|
||||||
@@ -19,6 +27,28 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
|
|||||||
const status = useSystemStatus();
|
const status = useSystemStatus();
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isVersionModalOpen, setIsVersionModalOpen] = useState(false);
|
||||||
|
const [latestVersion, setLatestVersion] = useState<{
|
||||||
|
version: string;
|
||||||
|
body: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLatestVersion().then(resp => {
|
||||||
|
if (resp.latest_version && resp.body) {
|
||||||
|
setLatestVersion({
|
||||||
|
version: resp.latest_version,
|
||||||
|
body: resp.body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showVersionModal = () => {
|
||||||
|
setIsVersionModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasUpdate = latestVersion && latestVersion.version !== status?.version;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sider
|
<Sider
|
||||||
@@ -105,29 +135,72 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px 8px',
|
padding: '12px 8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
flexDirection: 'column',
|
||||||
gap: 8,
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderTop: `1px solid ${token.colorBorderSecondary}`
|
borderTop: `1px solid ${token.colorBorderSecondary}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<div style={{
|
||||||
shape="circle"
|
fontSize: 12,
|
||||||
icon={<GithubOutlined />}
|
color: token.colorTextSecondary,
|
||||||
href="https://github.com/DrizzleTime/Foxel"
|
textAlign: 'center',
|
||||||
target="_blank"
|
height: 22,
|
||||||
/>
|
display: 'flex',
|
||||||
<Button
|
alignItems: 'center',
|
||||||
shape="circle"
|
justifyContent: 'center',
|
||||||
icon={<WechatOutlined />}
|
cursor: 'pointer'
|
||||||
onClick={() => setIsModalOpen(true)}
|
}} onClick={showVersionModal}>
|
||||||
/>
|
{hasUpdate ? (
|
||||||
<Button
|
<Tooltip title={`发现新版本: ${latestVersion?.version}`} placement={collapsed ? 'right' : 'top'}>
|
||||||
shape="circle"
|
<a href="https://github.com/DrizzleTime/Foxel/releases" target="_blank" rel="noopener noreferrer"
|
||||||
icon={<SendOutlined />}
|
style={{ textDecoration: 'none' }}>
|
||||||
href="https://t.me/+thDsBfyqJxZkNTU1"
|
{collapsed ? (
|
||||||
target="_blank"
|
<Tag icon={<WarningOutlined />} color="warning" style={{ marginInlineEnd: 0 }} />
|
||||||
/>
|
) : (
|
||||||
|
<Tag icon={<WarningOutlined />} color="warning">
|
||||||
|
{status?.version} - 有更新 [{latestVersion?.version}]
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
latestVersion ? (
|
||||||
|
<Tooltip title={`当前为最新版: ${status?.version}`} placement={collapsed ? 'right' : 'top'}>
|
||||||
|
{collapsed ? (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success" style={{ marginInlineEnd: 0 }} />
|
||||||
|
) : (
|
||||||
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||||
|
已是最新版
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
collapsed ? null : <Tag>{status?.version}</Tag>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: collapsed ? 'column' : 'row', gap: 8 }}>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
icon={<GithubOutlined />}
|
||||||
|
href="https://github.com/DrizzleTime/Foxel"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
icon={<WechatOutlined />}
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
shape="circle"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
href="https://t.me/+thDsBfyqJxZkNTU1"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -147,6 +220,22 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
open={isVersionModalOpen}
|
||||||
|
onCancel={() => setIsVersionModalOpen(false)}
|
||||||
|
title="版本信息"
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>当前版本: {status?.version}</p>
|
||||||
|
{latestVersion && (
|
||||||
|
<div>
|
||||||
|
<p>最新版本: {latestVersion.version}</p>
|
||||||
|
<ReactMarkdown>{latestVersion.body}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user