Compare commits

...

4 Commits

Author SHA1 Message Date
WrBug 532f4c3e25 fix: 修复系统更新 API 路由错误(/update/execute -> /update/update) 2026-01-21 05:30:48 +08:00
WrBug 04629e73b6 fix: 修复 TypeScript 编译错误
- 移除未使用的 getGitHubTagUrl 导入
- 移除未使用的 Divider、InfoCircleOutlined 导入
- 移除未使用的 Typography 组件解构
2026-01-21 05:18:42 +08:00
WrBug 696193c571 feat: 优化系统更新功能
- 系统更新模块移到系统设置页面最上方
- 版本号显示使用 Tag 格式(gitTag)
- 版本号 Tag 根据是否有新版本显示不同颜色(黄色=有新版本,绿色=无新版本)
- 版本号使用镂空样式,字号 8px
- 支持 Markdown 渲染更新内容
- 美化系统更新页面样式
- 在 Layout 中添加版本更新检查,有新版本时显示提示
2026-01-21 05:13:28 +08:00
WrBug 38e256c4fb fix: 修复前端构建时版本号未正确传递的问题
- 在 GitHub Actions 前端构建步骤中添加 VERSION、GIT_TAG 环境变量
- 确保版本号能正确注入到前端构建产物中
2026-01-21 04:52:32 +08:00
4 changed files with 341 additions and 122 deletions
+5
View File
@@ -111,11 +111,16 @@ jobs:
node-version: '18'
- name: Build Frontend
env:
VERSION: ${{ steps.extract_version.outputs.VERSION }}
GIT_TAG: ${{ steps.extract_version.outputs.TAG }}
GITHUB_REPO_URL: https://github.com/WrBug/PolyHermes
run: |
cd frontend
npm ci
npm run build
echo "✅ 前端构建完成"
echo "📦 版本信息: VERSION=${{ steps.extract_version.outputs.VERSION }}, GIT_TAG=${{ steps.extract_version.outputs.TAG }}"
du -sh dist/
# ============ 打包更新包 ============
+91 -38
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Layout as AntLayout, Menu, Drawer, Button, Modal } from 'antd'
import { Layout as AntLayout, Menu, Drawer, Button, Modal, Tag } from 'antd'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
import {
@@ -19,16 +19,37 @@ import {
TwitterOutlined,
CheckCircleOutlined,
SendOutlined,
ApiOutlined, NotificationOutlined
ApiOutlined,
NotificationOutlined
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import type { ReactNode } from 'react'
import { removeToken, getVersionText, getGitHubTagUrl } from '../utils'
import { removeToken, getVersionText, getVersionInfo } from '../utils'
import { wsManager } from '../services/websocket'
import { apiClient } from '../services/api'
import Logo from './Logo'
const { Header, Content, Sider } = AntLayout
// 添加动画样式
const style = document.createElement('style')
style.textContent = `
@keyframes versionUpdatePulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
`
if (!document.head.querySelector('style[data-version-update-animation]')) {
style.setAttribute('data-version-update-animation', 'true')
document.head.appendChild(style)
}
interface LayoutProps {
children: ReactNode
}
@@ -39,6 +60,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const location = useLocation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [hasUpdate, setHasUpdate] = useState(false)
// 获取当前选中的菜单项
const getSelectedKeys = (): string[] => {
@@ -72,6 +94,29 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
}
setOpenKeys(keys)
}, [location.pathname])
// 检查是否有新版本
useEffect(() => {
const checkUpdate = async () => {
try {
const response = await apiClient.get('/update/check')
if (response.data.code === 0 && response.data.data) {
setHasUpdate(response.data.data.hasUpdate || false)
}
} catch (error) {
// 静默失败,不影响页面使用
console.debug('检查更新失败:', error)
}
}
// 页面加载时检查一次
checkUpdate()
// 每5分钟检查一次
const interval = setInterval(checkUpdate, 5 * 60 * 1000)
return () => clearInterval(interval)
}, [])
const menuItems: MenuProps['items'] = [
{
@@ -210,27 +255,32 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
size="normal"
darkMode={true}
/>
<a
href={getGitHubTagUrl()}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
const version = getVersionText()
if (version === 'dev') {
e.preventDefault()
<Tag
color={hasUpdate ? 'warning' : 'success'}
onClick={() => {
if (hasUpdate) {
navigate('/system-settings')
}
}}
bordered={false}
style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '12px',
fontWeight: 'normal',
textDecoration: 'none',
cursor: getVersionText() === 'dev' ? 'default' : 'pointer'
cursor: hasUpdate ? 'pointer' : 'default',
fontSize: '8px',
padding: '1px 6px',
margin: 0,
background: 'transparent',
border: `1px solid ${hasUpdate ? '#faad14' : '#52c41a'}`,
borderRadius: '4px',
color: hasUpdate ? '#faad14' : '#52c41a',
lineHeight: '1.4',
display: 'inline-flex',
alignItems: 'center',
verticalAlign: 'middle'
}}
title={getVersionText() === 'dev' ? '' : '查看版本发布'}
title={hasUpdate ? '有新版本可用,点击前往系统更新' : '当前已是最新版本'}
>
v{getVersionText()}
</a>
{getVersionInfo().gitTag || `v${getVersionText()}`}
</Tag>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<a
@@ -322,34 +372,37 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
marginBottom: '12px',
textAlign: 'center',
display: 'flex',
alignItems: 'flex-end',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}>
<span>PolyHermes</span>
<a
href={getGitHubTagUrl()}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
const version = getVersionText()
if (version === 'dev') {
e.preventDefault()
<Tag
color={hasUpdate ? 'warning' : 'success'}
onClick={() => {
if (hasUpdate) {
navigate('/system-settings')
}
}}
bordered={false}
style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '12px',
fontWeight: 'normal',
textDecoration: 'none',
cursor: getVersionText() === 'dev' ? 'default' : 'pointer',
lineHeight: '1',
paddingBottom: '2px'
cursor: hasUpdate ? 'pointer' : 'default',
fontSize: '8px',
padding: '1px 6px',
margin: 0,
background: 'transparent',
border: `1px solid ${hasUpdate ? '#faad14' : '#52c41a'}`,
borderRadius: '4px',
color: hasUpdate ? '#faad14' : '#52c41a',
lineHeight: '1.4',
display: 'inline-flex',
alignItems: 'center',
verticalAlign: 'middle'
}}
title={getVersionText() === 'dev' ? '' : '查看版本发布'}
title={hasUpdate ? '有新版本可用,点击前往系统更新' : '当前已是最新版本'}
>
v{getVersionText()}
</a>
{getVersionInfo().gitTag || `v${getVersionText()}`}
</Tag>
</div>
<div style={{
display: 'flex',
+3 -3
View File
@@ -495,6 +495,9 @@ const SystemSettings: React.FC = () => {
<Title level={2} style={{ margin: 0 }}>{t('systemSettings.title') || '通用设置'}</Title>
</div>
{/* 系统更新 */}
<SystemUpdate />
{/* 第一部分:多语言 */}
<Card
title={
@@ -890,9 +893,6 @@ const SystemSettings: React.FC = () => {
)}
</Card>
{/* 第五部分:系统更新 */}
<SystemUpdate />
</div>
)
}
+242 -81
View File
@@ -1,15 +1,15 @@
import { useState, useEffect } from 'react'
import { Card, Button, Spin, Progress, Alert, Space, Typography, Divider, Tag, Modal, message } from 'antd'
import { Card, Button, Spin, Progress, Alert, Space, Tag, Modal, message } from 'antd'
import {
CloudUploadOutlined,
ReloadOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined
ExclamationCircleOutlined
} from '@ant-design/icons'
import { apiClient } from '../services/api'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
const { Title, Text, Paragraph } = Typography
interface UpdateInfo {
hasUpdate: boolean
@@ -113,7 +113,7 @@ const SystemUpdate: React.FC = () => {
cancelText: '取消',
onOk: async () => {
try {
const response = await apiClient.post('/update/execute', {})
const response = await apiClient.post('/update/update', {})
const data = response.data
if (data.code === 0) {
@@ -172,145 +172,302 @@ const SystemUpdate: React.FC = () => {
<Card
title={
<Space>
<CloudUploadOutlined />
<span></span>
<CloudUploadOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
<span style={{ fontSize: '16px', fontWeight: 600 }}></span>
</Space>
}
style={{ marginBottom: '16px' }}
style={{
marginBottom: '16px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 当前版本信息 */}
<div>
<Title level={5}></Title>
<Space>
<Tag color="blue" style={{ fontSize: '14px', padding: '4px 12px' }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '8px',
color: '#fff'
}}>
<div>
<div style={{ fontSize: '13px', opacity: 0.9, marginBottom: '4px' }}>
</div>
<div style={{ fontSize: '20px', fontWeight: 600 }}>
v{currentVersion || 'unknown'}
</Tag>
</Space>
</div>
</div>
<CheckCircleOutlined style={{ fontSize: '32px', opacity: 0.8 }} />
</div>
<Divider />
{/* 更新状态 */}
{updateStatus.updating && (
<Alert
message="系统正在更新"
message={
<span style={{ fontSize: '15px', fontWeight: 500 }}></span>
}
description={
<div>
<p>{updateStatus.message}</p>
<div style={{ marginTop: '12px' }}>
<div style={{
marginBottom: '12px',
fontSize: '14px',
color: '#595959'
}}>
{updateStatus.message}
</div>
<Progress
percent={updateStatus.progress}
status="active"
strokeColor={{ '0%': '#108ee9', '100%': '#87d068' }}
strokeColor={{
'0%': '#667eea',
'50%': '#764ba2',
'100%': '#f093fb'
}}
strokeWidth={8}
showInfo
format={(percent) => `${percent}%`}
/>
</div>
}
type="info"
showIcon
icon={<Spin />}
style={{
borderRadius: '8px',
border: '1px solid #91d5ff'
}}
/>
)}
{updateStatus.error && (
<Alert
message="更新失败"
description={updateStatus.error}
message={<span style={{ fontSize: '15px', fontWeight: 500 }}></span>}
description={
<div style={{
marginTop: '8px',
fontSize: '14px',
color: '#595959'
}}>
{updateStatus.error}
</div>
}
type="error"
showIcon
closable
onClose={() => setUpdateStatus(prev => ({ ...prev, error: null }))}
style={{
borderRadius: '8px'
}}
/>
)}
{/* 检查更新 */}
{!updateStatus.updating && (
<Space>
<div>
<Button
type="primary"
size="large"
icon={<ReloadOutlined />}
onClick={handleCheckUpdate}
loading={updateChecking}
style={{
height: '40px',
borderRadius: '6px',
fontWeight: 500,
boxShadow: '0 2px 4px rgba(24, 144, 255, 0.2)'
}}
>
</Button>
{updateInfo && !updateInfo.hasUpdate && (
<Alert
message="当前已是最新版本"
message={
<span style={{ fontSize: '15px', fontWeight: 500 }}>
</span>
}
type="success"
showIcon
icon={<CheckCircleOutlined />}
style={{
marginTop: '12px',
borderRadius: '8px',
border: '1px solid #b7eb8f'
}}
/>
)}
</Space>
</div>
)}
{/* 更新信息 */}
{updateInfo && updateInfo.hasUpdate && !updateStatus.updating && (
<Alert
message={
<Space>
<span>:</span>
<Tag color="green" style={{ fontSize: '14px' }}>
v{updateInfo.latestVersion}
</Tag>
{updateInfo.prerelease && (
<Tag color="orange">Pre-release</Tag>
)}
</Space>
}
description={
<div style={{ marginTop: '12px' }}>
<Paragraph>
<Text strong></Text>
<Text type="secondary">{formatDate(updateInfo.publishedAt)}</Text>
</Paragraph>
{updateInfo.releaseNotes && (
<div>
<Text strong></Text>
<div style={{
marginTop: '8px',
padding: '12px',
background: '#f5f5f5',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto'
}}>
<pre style={{
margin: 0,
whiteSpace: 'pre-wrap',
fontFamily: 'inherit'
}}>
{updateInfo.releaseNotes}
</pre>
</div>
</div>
)}
<div style={{ marginTop: '16px' }}>
<Button
type="primary"
icon={<CloudUploadOutlined />}
onClick={handleExecuteUpdate}
<div style={{
padding: '20px',
background: 'linear-gradient(135deg, #fff5f5 0%, #fff1f0 100%)',
borderRadius: '8px',
border: '1px solid #ffccc7'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '16px',
paddingBottom: '16px',
borderBottom: '1px solid #ffccc7'
}}>
<div>
<div style={{
fontSize: '13px',
color: '#8c8c8c',
marginBottom: '6px'
}}>
</div>
<Space size="small">
<Tag
color="success"
style={{
fontSize: '16px',
padding: '4px 16px',
fontWeight: 600,
borderRadius: '4px'
}}
>
</Button>
v{updateInfo.latestVersion}
</Tag>
{updateInfo.prerelease && (
<Tag
color="orange"
style={{
fontSize: '12px',
padding: '4px 12px',
borderRadius: '4px'
}}
>
Pre-release
</Tag>
)}
</Space>
</div>
<CloudUploadOutlined style={{
fontSize: '32px',
color: '#ff4d4f',
opacity: 0.8
}} />
</div>
<div style={{ marginBottom: '16px' }}>
<div style={{
fontSize: '13px',
color: '#8c8c8c',
marginBottom: '4px'
}}>
</div>
<div style={{
fontSize: '14px',
color: '#595959'
}}>
{formatDate(updateInfo.publishedAt)}
</div>
</div>
{updateInfo.releaseNotes && (
<div style={{ marginBottom: '20px' }}>
<div style={{
fontSize: '13px',
color: '#8c8c8c',
marginBottom: '8px',
fontWeight: 500
}}>
</div>
<div style={{
padding: '16px',
background: '#fff',
borderRadius: '6px',
border: '1px solid #e8e8e8',
maxHeight: '500px',
overflowY: 'auto',
lineHeight: '1.6',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.06)'
}}>
<div style={{
color: '#262626',
fontSize: '14px'
}}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h1: ({node, ...props}) => <h1 style={{ fontSize: '20px', fontWeight: 600, marginTop: '16px', marginBottom: '12px', color: '#262626', borderBottom: '2px solid #e8e8e8', paddingBottom: '8px' }} {...props} />,
h2: ({node, ...props}) => <h2 style={{ fontSize: '18px', fontWeight: 600, marginTop: '16px', marginBottom: '10px', color: '#262626' }} {...props} />,
h3: ({node, ...props}) => <h3 style={{ fontSize: '16px', fontWeight: 600, marginTop: '14px', marginBottom: '8px', color: '#262626' }} {...props} />,
h4: ({node, ...props}) => <h4 style={{ fontSize: '15px', fontWeight: 600, marginTop: '12px', marginBottom: '6px', color: '#262626' }} {...props} />,
p: ({node, ...props}) => <p style={{ marginBottom: '12px', color: '#595959' }} {...props} />,
ul: ({node, ...props}) => <ul style={{ marginBottom: '12px', paddingLeft: '24px', color: '#595959' }} {...props} />,
ol: ({node, ...props}) => <ol style={{ marginBottom: '12px', paddingLeft: '24px', color: '#595959' }} {...props} />,
li: ({node, ...props}) => <li style={{ marginBottom: '6px', lineHeight: '1.6' }} {...props} />,
code: ({node, inline, ...props}: any) =>
inline
? <code style={{ background: '#f0f0f0', padding: '2px 6px', borderRadius: '3px', fontSize: '13px', fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace', color: '#d73a49' }} {...props} />
: <code style={{ display: 'block', background: '#282c34', color: '#abb2bf', padding: '12px', borderRadius: '4px', overflowX: 'auto', fontSize: '13px', fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, monospace', marginBottom: '12px', lineHeight: '1.5' }} {...props} />,
pre: ({node, ...props}) => <pre style={{ background: '#282c34', borderRadius: '4px', padding: '12px', overflowX: 'auto', marginBottom: '12px' }} {...props} />,
blockquote: ({node, ...props}) => <blockquote style={{ borderLeft: '4px solid #1890ff', paddingLeft: '12px', margin: '12px 0', color: '#8c8c8c', fontStyle: 'italic' }} {...props} />,
a: ({node, ...props}) => <a style={{ color: '#1890ff', textDecoration: 'none' }} target="_blank" rel="noopener noreferrer" {...props} />,
strong: ({node, ...props}) => <strong style={{ fontWeight: 600, color: '#262626' }} {...props} />,
table: ({node, ...props}) => <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '12px' }} {...props} />,
th: ({node, ...props}) => <th style={{ border: '1px solid #e8e8e8', padding: '8px 12px', background: '#fafafa', fontWeight: 600, textAlign: 'left' }} {...props} />,
td: ({node, ...props}) => <td style={{ border: '1px solid #e8e8e8', padding: '8px 12px' }} {...props} />,
hr: ({node, ...props}) => <hr style={{ border: 'none', borderTop: '1px solid #e8e8e8', margin: '16px 0' }} {...props} />
}}
>
{updateInfo.releaseNotes}
</ReactMarkdown>
</div>
</div>
</div>
}
type="warning"
showIcon
icon={<InfoCircleOutlined />}
/>
)}
<Button
type="primary"
size="large"
icon={<CloudUploadOutlined />}
onClick={handleExecuteUpdate}
block
style={{
height: '44px',
borderRadius: '6px',
fontWeight: 500,
fontSize: '15px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.4)'
}}
>
v{updateInfo.latestVersion}
</Button>
</div>
)}
{/* 使用提示 */}
{!updateStatus.updating && (
{!updateStatus.updating && !(updateInfo && updateInfo.hasUpdate) && (
<Alert
message="使用说明"
message={
<span style={{ fontSize: '15px', fontWeight: 500 }}>使</span>
}
description={
<ul style={{ marginBottom: 0, paddingLeft: '20px' }}>
<ul style={{
marginBottom: 0,
paddingLeft: '20px',
fontSize: '14px',
color: '#595959',
lineHeight: '1.8'
}}>
<li>"检查更新"</li>
<li>30-60</li>
<li></li>
@@ -319,6 +476,10 @@ const SystemUpdate: React.FC = () => {
}
type="info"
showIcon
style={{
borderRadius: '8px',
border: '1px solid #91d5ff'
}}
/>
)}
</Space>