mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: enhance user experience with version info (#69)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check } from 'lucide-react'
|
import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info } from 'lucide-react'
|
||||||
|
|
||||||
export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check }
|
export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info }
|
||||||
|
|
||||||
const LucideIcons = {
|
const LucideIcons = {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -8,7 +8,11 @@ const LucideIcons = {
|
|||||||
Edit,
|
Edit,
|
||||||
Trash,
|
Trash,
|
||||||
Copy,
|
Copy,
|
||||||
Check
|
Check,
|
||||||
|
User,
|
||||||
|
Settings,
|
||||||
|
LogOut,
|
||||||
|
Info
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LucideIcons
|
export default LucideIcons
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import ThemeSwitch from '@/components/ui/ThemeSwitch';
|
import ThemeSwitch from '@/components/ui/ThemeSwitch';
|
||||||
|
|
||||||
@@ -10,13 +9,7 @@ interface HeaderProps {
|
|||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const { auth } = useAuth();
|
||||||
const { auth, logout } = useAuth();
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
logout();
|
|
||||||
navigate('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white dark:bg-gray-800 shadow-sm z-10">
|
<header className="bg-white dark:bg-gray-800 shadow-sm z-10">
|
||||||
@@ -37,25 +30,10 @@ const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
|||||||
<h1 className="ml-4 text-xl font-bold text-gray-900 dark:text-white">{t('app.title')}</h1>
|
<h1 className="ml-4 text-xl font-bold text-gray-900 dark:text-white">{t('app.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 用户信息和操作 */}
|
{/* Theme Switch */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{/* Theme Switch */}
|
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
|
|
||||||
{auth.user && (
|
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
{t('app.welcomeUser', { username: auth.user.username })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="px-3 py-1.5 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100 rounded hover:bg-red-200 dark:hover:bg-red-800 text-sm"
|
|
||||||
>
|
|
||||||
{t('app.logout')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
|
import UserProfileMenu from '@/components/ui/UserProfileMenu';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
@@ -16,6 +17,9 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Application version from package.json (accessed via Vite environment variables)
|
||||||
|
const appVersion = import.meta.env.PACKAGE_VERSION as string;
|
||||||
|
|
||||||
// Menu item configuration
|
// Menu item configuration
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
@@ -64,42 +68,41 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
label: t('nav.settings'),
|
|
||||||
icon: (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={`bg-white dark:bg-gray-800 shadow-sm transition-all duration-300 ease-in-out ${
|
className={`bg-white dark:bg-gray-800 shadow-sm transition-all duration-300 ease-in-out flex flex-col h-full relative ${
|
||||||
collapsed ? 'w-16' : 'w-64'
|
collapsed ? 'w-16' : 'w-64'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<nav className="p-3 space-y-1">
|
{/* Scrollable navigation area */}
|
||||||
{menuItems.map((item) => (
|
<div className="overflow-y-auto flex-grow">
|
||||||
<NavLink
|
<nav className="p-3 space-y-1">
|
||||||
key={item.path}
|
{menuItems.map((item) => (
|
||||||
to={item.path}
|
<NavLink
|
||||||
className={({ isActive }) =>
|
key={item.path}
|
||||||
`flex items-center px-3 py-2 rounded-md transition-colors ${
|
to={item.path}
|
||||||
isActive
|
className={({ isActive }) =>
|
||||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
|
`flex items-center px-3 py-2 rounded-md transition-colors ${
|
||||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
isActive
|
||||||
}`
|
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
|
||||||
}
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||||
end={item.path === '/'}
|
}`
|
||||||
>
|
}
|
||||||
<span className="flex-shrink-0">{item.icon}</span>
|
end={item.path === '/'}
|
||||||
{!collapsed && <span className="ml-3">{item.label}</span>}
|
>
|
||||||
</NavLink>
|
<span className="flex-shrink-0">{item.icon}</span>
|
||||||
))}
|
{!collapsed && <span className="ml-3">{item.label}</span>}
|
||||||
</nav>
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User profile menu fixed at the bottom */}
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-800">
|
||||||
|
<UserProfileMenu collapsed={collapsed} version={appVersion} />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
92
frontend/src/components/ui/AboutDialog.tsx
Normal file
92
frontend/src/components/ui/AboutDialog.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AboutDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AboutDialog: React.FC<AboutDialogProps> = ({ isOpen, onClose, version }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [hasNewVersion, setHasNewVersion] = useState(false);
|
||||||
|
const [latestVersion, setLatestVersion] = useState("");
|
||||||
|
|
||||||
|
// Check if there's a new version available
|
||||||
|
// In a real application, this would make an API call to check for updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
// Mock implementation - in real implementation, you would fetch from an API
|
||||||
|
const checkForUpdates = async () => {
|
||||||
|
try {
|
||||||
|
// This is a placeholder - in a real app you would call an API
|
||||||
|
// const response = await fetch('/api/check-updates');
|
||||||
|
// const data = await response.json();
|
||||||
|
|
||||||
|
// For demo purposes, we'll just pretend there's a new version
|
||||||
|
// TODO: Replace this placeholder logic with a real API call to check for updates
|
||||||
|
setHasNewVersion(false);
|
||||||
|
setLatestVersion("0.0.28"); // Just a higher version for demo
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check for updates:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkForUpdates();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 bg-opacity-30 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-md w-full">
|
||||||
|
<div className="p-6 relative">
|
||||||
|
{/* Close button (X) in the top-right corner */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400"
|
||||||
|
aria-label={t('common.close')}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
|
||||||
|
{t('about.title')}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
{t('about.currentVersion')}:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{version}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasNewVersion && (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900 p-3 rounded">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="h-5 w-5 text-blue-600 dark:text-blue-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||||
|
{t('about.newVersion')} (v{latestVersion})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AboutDialog;
|
||||||
127
frontend/src/components/ui/UserProfileMenu.tsx
Normal file
127
frontend/src/components/ui/UserProfileMenu.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { User, Settings, LogOut, Info } from 'lucide-react';
|
||||||
|
import AboutDialog from './AboutDialog';
|
||||||
|
|
||||||
|
interface UserProfileMenuProps {
|
||||||
|
collapsed: boolean;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { auth, logout } = useAuth();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [showNewVersionInfo, setShowNewVersionInfo] = useState(false);
|
||||||
|
const [showAboutDialog, setShowAboutDialog] = useState(false);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Check for new version - this could be replaced with a real API call
|
||||||
|
useEffect(() => {
|
||||||
|
// Mock implementation - in a real app, you would check against an API
|
||||||
|
const checkForNewVersion = async () => {
|
||||||
|
// For demo purposes, we're just showing the notification
|
||||||
|
// In real implementation, you would compare current version with latest version
|
||||||
|
setShowNewVersionInfo(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkForNewVersion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close the menu when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSettingsClick = () => {
|
||||||
|
navigate('/settings');
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoutClick = () => {
|
||||||
|
logout();
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAboutClick = () => {
|
||||||
|
setShowAboutDialog(true);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menuRef} className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className={`flex ${collapsed ? 'justify-center' : 'items-center'} w-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors rounded-md ${isOpen ? 'bg-gray-100 dark:bg-gray-700' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 relative">
|
||||||
|
<div className="w-7 h-7 flex items-center justify-center rounded-full border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700">
|
||||||
|
<User className="h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||||
|
</div>
|
||||||
|
{showNewVersionInfo && (
|
||||||
|
<span className="absolute -top-1 -right-1 block w-2 h-2 bg-red-500 rounded-full"></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<div className="ml-3 flex flex-col items-start">
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{auth.user?.username || t('auth.user')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute top-0 transform -translate-y-full left-0 w-48 bg-white dark:bg-gray-800 shadow-lg rounded-md py-1 z-50">
|
||||||
|
<button
|
||||||
|
onClick={handleSettingsClick}
|
||||||
|
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
{t('nav.settings')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleAboutClick}
|
||||||
|
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 relative"
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4 mr-2" />
|
||||||
|
{t('about.title')}
|
||||||
|
({version})
|
||||||
|
{showNewVersionInfo && (
|
||||||
|
<span className="absolute top-2 right-4 block w-2 h-2 bg-red-500 rounded-full"></span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogoutClick}
|
||||||
|
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
{t('app.logout')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* About dialog */}
|
||||||
|
<AboutDialog
|
||||||
|
isOpen={showAboutDialog}
|
||||||
|
onClose={() => setShowAboutDialog(false)}
|
||||||
|
version={version}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfileMenu;
|
||||||
@@ -13,7 +13,7 @@ const MainLayout: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-gray-900">
|
<div className="flex flex-col h-screen bg-gray-100 dark:bg-gray-900">
|
||||||
{/* 顶部导航 */}
|
{/* 顶部导航 */}
|
||||||
<Header onToggleSidebar={toggleSidebar} />
|
<Header onToggleSidebar={toggleSidebar} />
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,16 @@
|
|||||||
"welcomeUser": "Welcome, {{username}}",
|
"welcomeUser": "Welcome, {{username}}",
|
||||||
"name": "MCP Hub"
|
"name": "MCP Hub"
|
||||||
},
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About",
|
||||||
|
"versionInfo": "MCP Hub Version: {{version}}",
|
||||||
|
"newVersion": "New version available!",
|
||||||
|
"currentVersion": "Current version"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"viewProfile": "View profile",
|
||||||
|
"userCenter": "User Center"
|
||||||
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"title": "Theme",
|
"title": "Theme",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
@@ -105,7 +115,8 @@
|
|||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copySuccess": "Copied to clipboard",
|
"copySuccess": "Copied to clipboard",
|
||||||
"copyFailed": "Copy failed"
|
"copyFailed": "Copy failed",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
|||||||
@@ -12,6 +12,16 @@
|
|||||||
"welcomeUser": "欢迎, {{username}}",
|
"welcomeUser": "欢迎, {{username}}",
|
||||||
"name": "MCP Hub"
|
"name": "MCP Hub"
|
||||||
},
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "关于",
|
||||||
|
"versionInfo": "MCP Hub 版本: {{version}}",
|
||||||
|
"newVersion": "有新版本可用!",
|
||||||
|
"currentVersion": "当前版本"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"viewProfile": "查看个人中心",
|
||||||
|
"userCenter": "个人中心"
|
||||||
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"title": "主题",
|
"title": "主题",
|
||||||
"light": "浅色",
|
"light": "浅色",
|
||||||
@@ -106,7 +116,8 @@
|
|||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"copySuccess": "已复制到剪贴板",
|
"copySuccess": "已复制到剪贴板",
|
||||||
"copyFailed": "复制失败"
|
"copyFailed": "复制失败",
|
||||||
|
"close": "关闭"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "仪表盘",
|
"dashboard": "仪表盘",
|
||||||
|
|||||||
9
frontend/src/vite-env.d.ts
vendored
Normal file
9
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: {
|
||||||
|
readonly PACKAGE_VERSION: string;
|
||||||
|
// Add other custom env variables here if needed
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,6 +2,13 @@ import { defineConfig } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
// Import the package.json to get the version
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
// Get package.json version
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -11,6 +18,10 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
// Make package version available as global variable
|
||||||
|
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samanhappy/mcphub",
|
"name": "@samanhappy/mcphub",
|
||||||
"version": "0.0.27",
|
"version": "0.5.4",
|
||||||
"description": "A hub server for mcp servers",
|
"description": "A hub server for mcp servers",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Reference in New Issue
Block a user