214 lines
8.1 KiB
TypeScript
214 lines
8.1 KiB
TypeScript
import { ReactNode, useEffect, useState } from 'react';
|
||
import { Link, useLocation } from 'react-router-dom';
|
||
import { useAppStore, getStatusColor } from '../stores/appStore';
|
||
import styles from './Layout.module.css';
|
||
|
||
interface LayoutProps {
|
||
children: ReactNode;
|
||
}
|
||
|
||
const navItems = [
|
||
{ path: '/', label: '我的钱包', icon: '🔐' },
|
||
{ path: '/create', label: '创建钱包', icon: '➕' },
|
||
{ path: '/join', label: '加入创建', icon: '🤝' },
|
||
{ path: '/cosign/join', label: '参与签名', icon: '🔥' },
|
||
{ path: '/settings', label: '设置', icon: '⚙️' },
|
||
];
|
||
|
||
export default function Layout({ children }: LayoutProps) {
|
||
const location = useLocation();
|
||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||
const [kavaNetwork, setKavaNetwork] = useState<'mainnet' | 'testnet'>('mainnet');
|
||
|
||
const { environment, operation, checkAllServices, appReady } = useAppStore();
|
||
|
||
// 启动时检测环境和获取网络
|
||
useEffect(() => {
|
||
checkAllServices();
|
||
// 获取当前 Kava 网络
|
||
const loadNetwork = () => {
|
||
// 优先从 localStorage 读取(与 transaction.ts 保持一致)
|
||
const storedNetwork = localStorage.getItem('kava_network') as 'mainnet' | 'testnet' | null;
|
||
if (storedNetwork) {
|
||
setKavaNetwork(storedNetwork);
|
||
} else {
|
||
// 后备:从 Electron API 读取
|
||
window.electronAPI?.kava.getNetwork().then(result => {
|
||
setKavaNetwork(result.network);
|
||
});
|
||
}
|
||
};
|
||
|
||
loadNetwork();
|
||
|
||
// 监听 localStorage 变化(当其他标签页切换网络时触发)
|
||
const handleStorageChange = (e: StorageEvent) => {
|
||
if (e.key === 'kava_network' && e.newValue) {
|
||
setKavaNetwork(e.newValue as 'mainnet' | 'testnet');
|
||
}
|
||
};
|
||
|
||
// 监听同一窗口的自定义事件(Settings 页面切换网络时触发)
|
||
const handleCustomNetworkChange = (e: Event) => {
|
||
const customEvent = e as CustomEvent<{ network: 'mainnet' | 'testnet' }>;
|
||
setKavaNetwork(customEvent.detail.network);
|
||
};
|
||
|
||
window.addEventListener('storage', handleStorageChange);
|
||
window.addEventListener('kava-network-change', handleCustomNetworkChange);
|
||
|
||
return () => {
|
||
window.removeEventListener('storage', handleStorageChange);
|
||
window.removeEventListener('kava-network-change', handleCustomNetworkChange);
|
||
};
|
||
}, [checkAllServices]);
|
||
|
||
const handleRefresh = async () => {
|
||
setIsRefreshing(true);
|
||
await checkAllServices();
|
||
setIsRefreshing(false);
|
||
};
|
||
|
||
const getOperationTitle = () => {
|
||
if (operation.type === 'keygen') return '密钥生成中';
|
||
if (operation.type === 'sign') return '签名进行中';
|
||
return '操作进行中';
|
||
};
|
||
|
||
return (
|
||
<div className={styles.layout}>
|
||
<nav className={styles.sidebar}>
|
||
<div className={styles.logo}>
|
||
<span className={styles.logoIcon}>🍈</span>
|
||
<span className={styles.logoText}>绿积分共管账户</span>
|
||
</div>
|
||
<ul className={styles.navList}>
|
||
{navItems.map((item) => (
|
||
<li key={item.path}>
|
||
<Link
|
||
to={item.path}
|
||
className={`${styles.navItem} ${
|
||
location.pathname === item.path ? styles.navItemActive : ''
|
||
}`}
|
||
>
|
||
<span className={styles.navIcon}>{item.icon}</span>
|
||
<span className={styles.navLabel}>{item.label}</span>
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<div className={styles.footer}>
|
||
{/* 状态面板 */}
|
||
<div className={styles.statusPanel}>
|
||
{/* 消息路由 */}
|
||
<div className={styles.statusRow}>
|
||
<span className={styles.statusLabel}>消息路由</span>
|
||
<span className={styles.statusValue}>
|
||
<span
|
||
className={styles.statusDot}
|
||
style={{ backgroundColor: getStatusColor(environment.messageRouter.status) }}
|
||
/>
|
||
<span className={styles.statusText} title={environment.messageRouter.message}>
|
||
{environment.messageRouter.status === 'connected'
|
||
? '已连接'
|
||
: environment.messageRouter.status === 'checking'
|
||
? '检测中...'
|
||
: environment.messageRouter.status === 'error'
|
||
? '失败'
|
||
: '未连接'}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
{/* Kava API */}
|
||
<div className={styles.statusRow}>
|
||
<span className={styles.statusLabel}>
|
||
Kava API
|
||
<span className={kavaNetwork === 'testnet' ? styles.networkBadgeTestnet : styles.networkBadgeMainnet}>
|
||
{kavaNetwork === 'testnet' ? '测试网' : '主网'}
|
||
</span>
|
||
</span>
|
||
<span className={styles.statusValue}>
|
||
<span
|
||
className={styles.statusDot}
|
||
style={{ backgroundColor: getStatusColor(environment.kavaApi.status) }}
|
||
/>
|
||
<span className={styles.statusText} title={environment.kavaApi.message}>
|
||
{environment.kavaApi.status === 'connected'
|
||
? '已连接'
|
||
: environment.kavaApi.status === 'checking'
|
||
? '检测中...'
|
||
: environment.kavaApi.status === 'error'
|
||
? '失败'
|
||
: '未连接'}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
{/* 本地存储 */}
|
||
<div className={styles.statusRow}>
|
||
<span className={styles.statusLabel}>本地存储</span>
|
||
<span className={styles.statusValue}>
|
||
<span
|
||
className={styles.statusDot}
|
||
style={{ backgroundColor: getStatusColor(environment.database.status) }}
|
||
/>
|
||
<span className={styles.statusText} title={environment.database.message}>
|
||
{environment.database.status === 'connected'
|
||
? environment.database.message
|
||
: environment.database.status === 'checking'
|
||
? '检测中...'
|
||
: environment.database.status === 'error'
|
||
? '错误'
|
||
: '未知'}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
{/* 刷新按钮 */}
|
||
<div className={styles.statusRow}>
|
||
<span className={styles.statusLabel}>
|
||
{appReady === 'ready' ? '✅ 就绪' : appReady === 'error' ? '⚠️ 部分异常' : '🔄 初始化中'}
|
||
</span>
|
||
<button
|
||
className={`${styles.refreshButton} ${isRefreshing ? styles.spinning : ''}`}
|
||
onClick={handleRefresh}
|
||
disabled={isRefreshing}
|
||
title="重新检测"
|
||
>
|
||
🔄
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作进度面板 */}
|
||
{(operation.status === 'connecting' || operation.status === 'in_progress') && (
|
||
<div className={styles.operationPanel}>
|
||
<div className={styles.operationHeader}>
|
||
<span className={styles.operationSpinner} />
|
||
<span>{getOperationTitle()}</span>
|
||
</div>
|
||
{operation.currentStep !== undefined && operation.totalSteps !== undefined && (
|
||
<>
|
||
<div className={styles.operationProgress}>
|
||
<div
|
||
className={styles.operationProgressBar}
|
||
style={{
|
||
width: `${(operation.currentStep / operation.totalSteps) * 100}%`,
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className={styles.operationStep}>
|
||
{operation.stepDescription || `步骤 ${operation.currentStep}/${operation.totalSteps}`}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</nav>
|
||
<main className={styles.main}>{children}</main>
|
||
</div>
|
||
);
|
||
}
|