feat(service-party-app): 添加应用状态检测和启动检查功能
## 新增功能 ### 1. 启动检测页面 (StartupCheck) - 应用启动时显示环境检测界面 - 检测三个核心服务: 本地数据库、消息路由、Kava API - 检测完成后自动进入主界面 (1.5秒延迟) - 支持查看详细错误信息 - 即使部分服务异常也可进入应用 ### 2. 应用状态管理 (appStore) - 使用 Zustand 管理全局应用状态 - 跟踪各服务的连接状态: unknown/checking/connected/disconnected/error - 支持操作进度跟踪 (keygen/sign) - 提供状态辅助函数: getStatusColor, getStatusText, getOverallStatus ### 3. 侧边栏状态面板 - 实时显示三个服务的连接状态 - 显示当前操作进度 (keygen/sign 时) - 支持手动刷新检测 - 显示整体就绪状态 ## 新增文件 - src/stores/appStore.ts: 应用状态管理 - src/components/StartupCheck.tsx: 启动检测组件 - src/components/StartupCheck.module.css: 启动检测样式 ## 修改文件 - src/App.tsx: 集成启动检测流程 - src/components/Layout.tsx: 添加状态面板 - src/components/Layout.module.css: 状态面板样式 - src/types/electron.d.ts: 添加 metadata 字段兼容 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b2b9e821e
commit
f5cbc855f6
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { useState } from 'react';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
|
import StartupCheck from './components/StartupCheck';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Join from './pages/Join';
|
import Join from './pages/Join';
|
||||||
import Create from './pages/Create';
|
import Create from './pages/Create';
|
||||||
|
|
@ -8,6 +10,13 @@ import Sign from './pages/Sign';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [startupComplete, setStartupComplete] = useState(false);
|
||||||
|
|
||||||
|
// 显示启动检测页面
|
||||||
|
if (!startupComplete) {
|
||||||
|
return <StartupCheck onComplete={() => setStartupComplete(true)} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
|
||||||
|
|
@ -78,31 +78,119 @@
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionStatus {
|
/* 状态面板 */
|
||||||
|
.statusPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-sm);
|
justify-content: space-between;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusLabel {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusValue {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusDot {
|
.statusDot {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--text-disabled);
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusDot[data-status="connected"] {
|
.statusText {
|
||||||
background-color: var(--success-color);
|
color: var(--text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
max-width: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusDot[data-status="connecting"] {
|
/* 操作进度 */
|
||||||
background-color: var(--warning-color);
|
.operationPanel {
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusDot[data-status="disconnected"] {
|
.operationHeader {
|
||||||
background-color: var(--error-color);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationSpinner {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationProgress {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationProgressBar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationStep {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 刷新按钮 */
|
||||||
|
.refreshButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshButton:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshButton.spinning {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { useAppStore, getStatusColor } from '../stores/appStore';
|
||||||
import styles from './Layout.module.css';
|
import styles from './Layout.module.css';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
|
|
@ -16,6 +17,26 @@ const navItems = [
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const { environment, operation, checkAllServices, appReady } = useAppStore();
|
||||||
|
|
||||||
|
// 启动时检测环境
|
||||||
|
useEffect(() => {
|
||||||
|
checkAllServices();
|
||||||
|
}, [checkAllServices]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await checkAllServices();
|
||||||
|
setIsRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOperationTitle = () => {
|
||||||
|
if (operation.type === 'keygen') return '密钥生成中';
|
||||||
|
if (operation.type === 'sign') return '签名进行中';
|
||||||
|
return '操作进行中';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
|
|
@ -40,10 +61,108 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<div className={styles.connectionStatus}>
|
{/* 状态面板 */}
|
||||||
<span className={styles.statusDot} data-status="connected" />
|
<div className={styles.statusPanel}>
|
||||||
<span>已连接</span>
|
{/* 消息路由 */}
|
||||||
|
<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>
|
||||||
|
<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>
|
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main className={styles.main}>{children}</main>
|
<main className={styles.main}>{children}</main>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 var(--spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkIcon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkLabel {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkStatus {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.successIcon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningIcon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsButton {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsButton:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailRow {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailRow:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterButton {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterButton:hover {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAppStore, getStatusColor } from '../stores/appStore';
|
||||||
|
import styles from './StartupCheck.module.css';
|
||||||
|
|
||||||
|
interface StartupCheckProps {
|
||||||
|
onComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StartupCheck({ onComplete }: StartupCheckProps) {
|
||||||
|
const { environment, checkAllServices, appReady } = useAppStore();
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const runCheck = async () => {
|
||||||
|
await checkAllServices();
|
||||||
|
};
|
||||||
|
runCheck();
|
||||||
|
}, [checkAllServices]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 检测完成后自动进入主界面(成功或失败都进入)
|
||||||
|
if (appReady === 'ready' || appReady === 'error') {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onComplete();
|
||||||
|
}, 1500); // 1.5秒后自动进入
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [appReady, onComplete]);
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return '✅';
|
||||||
|
case 'checking':
|
||||||
|
return '🔄';
|
||||||
|
case 'error':
|
||||||
|
return '❌';
|
||||||
|
default:
|
||||||
|
return '⏳';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<span className={styles.logoIcon}>🍈</span>
|
||||||
|
<h1 className={styles.title}>绿积分共管账户服务</h1>
|
||||||
|
<p className={styles.subtitle}>榴莲皇后 · 分布式多方共管钱包</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 状态检测 */}
|
||||||
|
<div className={styles.checkList}>
|
||||||
|
<div className={styles.checkItem}>
|
||||||
|
<span className={styles.checkIcon}>{getStatusIcon(environment.database.status)}</span>
|
||||||
|
<span className={styles.checkLabel}>本地数据库</span>
|
||||||
|
<span
|
||||||
|
className={styles.checkStatus}
|
||||||
|
style={{ color: getStatusColor(environment.database.status) }}
|
||||||
|
>
|
||||||
|
{environment.database.status === 'connected'
|
||||||
|
? '正常'
|
||||||
|
: environment.database.status === 'checking'
|
||||||
|
? '检测中...'
|
||||||
|
: environment.database.status === 'error'
|
||||||
|
? '异常'
|
||||||
|
: '等待检测'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.checkItem}>
|
||||||
|
<span className={styles.checkIcon}>{getStatusIcon(environment.messageRouter.status)}</span>
|
||||||
|
<span className={styles.checkLabel}>消息路由服务</span>
|
||||||
|
<span
|
||||||
|
className={styles.checkStatus}
|
||||||
|
style={{ color: getStatusColor(environment.messageRouter.status) }}
|
||||||
|
>
|
||||||
|
{environment.messageRouter.status === 'connected'
|
||||||
|
? '已连接'
|
||||||
|
: environment.messageRouter.status === 'checking'
|
||||||
|
? '检测中...'
|
||||||
|
: environment.messageRouter.status === 'error'
|
||||||
|
? '连接失败'
|
||||||
|
: '等待检测'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.checkItem}>
|
||||||
|
<span className={styles.checkIcon}>{getStatusIcon(environment.kavaApi.status)}</span>
|
||||||
|
<span className={styles.checkLabel}>Kava 区块链 API</span>
|
||||||
|
<span
|
||||||
|
className={styles.checkStatus}
|
||||||
|
style={{ color: getStatusColor(environment.kavaApi.status) }}
|
||||||
|
>
|
||||||
|
{environment.kavaApi.status === 'connected'
|
||||||
|
? '已连接'
|
||||||
|
: environment.kavaApi.status === 'checking'
|
||||||
|
? '检测中...'
|
||||||
|
: environment.kavaApi.status === 'error'
|
||||||
|
? '连接失败'
|
||||||
|
: '等待检测'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 总体状态 */}
|
||||||
|
<div className={styles.summary}>
|
||||||
|
{appReady === 'initializing' && (
|
||||||
|
<>
|
||||||
|
<div className={styles.spinner} />
|
||||||
|
<span>正在初始化环境...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appReady === 'ready' && (
|
||||||
|
<>
|
||||||
|
<span className={styles.successIcon}>✅</span>
|
||||||
|
<span>环境检测完成,即将进入主界面</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appReady === 'error' && (
|
||||||
|
<>
|
||||||
|
<span className={styles.warningIcon}>⚠️</span>
|
||||||
|
<span>部分服务异常,但仍可使用基本功能</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详情按钮 */}
|
||||||
|
{(environment.messageRouter.message || environment.kavaApi.message) && (
|
||||||
|
<button
|
||||||
|
className={styles.detailsButton}
|
||||||
|
onClick={() => setShowDetails(!showDetails)}
|
||||||
|
>
|
||||||
|
{showDetails ? '隐藏详情' : '查看详情'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 详细信息 */}
|
||||||
|
{showDetails && (
|
||||||
|
<div className={styles.details}>
|
||||||
|
{environment.messageRouter.message && (
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>消息路由:</span>
|
||||||
|
<span className={styles.detailValue}>{environment.messageRouter.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{environment.kavaApi.message && (
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>Kava API:</span>
|
||||||
|
<span className={styles.detailValue}>{environment.kavaApi.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{environment.database.message && (
|
||||||
|
<div className={styles.detailRow}>
|
||||||
|
<span className={styles.detailLabel}>本地存储:</span>
|
||||||
|
<span className={styles.detailValue}>{environment.database.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 手动进入按钮 */}
|
||||||
|
{(appReady === 'ready' || appReady === 'error') && (
|
||||||
|
<button className={styles.enterButton} onClick={onComplete}>
|
||||||
|
进入应用
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 类型定义
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type ConnectionStatus = 'unknown' | 'checking' | 'connected' | 'disconnected' | 'error';
|
||||||
|
export type AppReadyState = 'initializing' | 'ready' | 'error';
|
||||||
|
export type OperationStatus = 'idle' | 'connecting' | 'in_progress' | 'completed' | 'failed';
|
||||||
|
|
||||||
|
export interface ServiceStatus {
|
||||||
|
status: ConnectionStatus;
|
||||||
|
message?: string;
|
||||||
|
lastChecked?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationProgress {
|
||||||
|
status: OperationStatus;
|
||||||
|
type?: 'keygen' | 'sign';
|
||||||
|
sessionId?: string;
|
||||||
|
currentStep?: number;
|
||||||
|
totalSteps?: number;
|
||||||
|
stepDescription?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentCheck {
|
||||||
|
messageRouter: ServiceStatus;
|
||||||
|
kavaApi: ServiceStatus;
|
||||||
|
database: ServiceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
// 应用就绪状态
|
||||||
|
appReady: AppReadyState;
|
||||||
|
appError?: string;
|
||||||
|
|
||||||
|
// 环境检测结果
|
||||||
|
environment: EnvironmentCheck;
|
||||||
|
|
||||||
|
// 当前操作进度
|
||||||
|
operation: OperationProgress;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setAppReady: (state: AppReadyState, error?: string) => void;
|
||||||
|
setMessageRouterStatus: (status: ServiceStatus) => void;
|
||||||
|
setKavaApiStatus: (status: ServiceStatus) => void;
|
||||||
|
setDatabaseStatus: (status: ServiceStatus) => void;
|
||||||
|
setOperation: (operation: Partial<OperationProgress>) => void;
|
||||||
|
resetOperation: () => void;
|
||||||
|
checkAllServices: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Store
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const useAppStore = create<AppState>((set, get) => ({
|
||||||
|
// 初始状态
|
||||||
|
appReady: 'initializing',
|
||||||
|
appError: undefined,
|
||||||
|
|
||||||
|
environment: {
|
||||||
|
messageRouter: { status: 'unknown' },
|
||||||
|
kavaApi: { status: 'unknown' },
|
||||||
|
database: { status: 'unknown' },
|
||||||
|
},
|
||||||
|
|
||||||
|
operation: {
|
||||||
|
status: 'idle',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setAppReady: (state, error) => set({ appReady: state, appError: error }),
|
||||||
|
|
||||||
|
setMessageRouterStatus: (status) =>
|
||||||
|
set((s) => ({
|
||||||
|
environment: { ...s.environment, messageRouter: status },
|
||||||
|
})),
|
||||||
|
|
||||||
|
setKavaApiStatus: (status) =>
|
||||||
|
set((s) => ({
|
||||||
|
environment: { ...s.environment, kavaApi: status },
|
||||||
|
})),
|
||||||
|
|
||||||
|
setDatabaseStatus: (status) =>
|
||||||
|
set((s) => ({
|
||||||
|
environment: { ...s.environment, database: status },
|
||||||
|
})),
|
||||||
|
|
||||||
|
setOperation: (operation) =>
|
||||||
|
set((s) => ({
|
||||||
|
operation: { ...s.operation, ...operation },
|
||||||
|
})),
|
||||||
|
|
||||||
|
resetOperation: () =>
|
||||||
|
set({
|
||||||
|
operation: { status: 'idle' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
checkAllServices: async () => {
|
||||||
|
const { setMessageRouterStatus, setKavaApiStatus, setDatabaseStatus, setAppReady } = get();
|
||||||
|
|
||||||
|
// 检测 Message Router
|
||||||
|
setMessageRouterStatus({ status: 'checking', message: '正在检测...' });
|
||||||
|
try {
|
||||||
|
if (window.electronAPI) {
|
||||||
|
const settings = await window.electronAPI.storage.getSettings();
|
||||||
|
const routerUrl = settings?.messageRouterUrl || 'mpc-grpc.szaiai.com:443';
|
||||||
|
const result = await window.electronAPI.grpc.testConnection(routerUrl);
|
||||||
|
if (result.success) {
|
||||||
|
setMessageRouterStatus({
|
||||||
|
status: 'connected',
|
||||||
|
message: routerUrl,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setMessageRouterStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: result.error || '连接失败',
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMessageRouterStatus({
|
||||||
|
status: 'disconnected',
|
||||||
|
message: '非 Electron 环境',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessageRouterStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: (error as Error).message,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测 Kava API
|
||||||
|
setKavaApiStatus({ status: 'checking', message: '正在检测...' });
|
||||||
|
try {
|
||||||
|
if (window.electronAPI) {
|
||||||
|
// 使用一个已知的测试地址查询余额来验证 API
|
||||||
|
const testAddress = 'kava1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnxwpql';
|
||||||
|
const result = await window.electronAPI.kava.getBalance(testAddress);
|
||||||
|
if (result.success || result.error?.includes('account')) {
|
||||||
|
// 即使账户不存在,API 能响应也说明连接正常
|
||||||
|
const config = await window.electronAPI.kava.getConfig();
|
||||||
|
setKavaApiStatus({
|
||||||
|
status: 'connected',
|
||||||
|
message: config?.lcdEndpoint || 'Kava Mainnet',
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setKavaApiStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: result.error || 'API 响应异常',
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setKavaApiStatus({
|
||||||
|
status: 'disconnected',
|
||||||
|
message: '非 Electron 环境',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setKavaApiStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: (error as Error).message,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测数据库
|
||||||
|
setDatabaseStatus({ status: 'checking', message: '正在检测...' });
|
||||||
|
try {
|
||||||
|
if (window.electronAPI) {
|
||||||
|
const result = await window.electronAPI.storage.listShares();
|
||||||
|
if (result.success) {
|
||||||
|
setDatabaseStatus({
|
||||||
|
status: 'connected',
|
||||||
|
message: `${result.data?.length || 0} 个钱包`,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setDatabaseStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: result.error || '数据库错误',
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setDatabaseStatus({
|
||||||
|
status: 'disconnected',
|
||||||
|
message: '非 Electron 环境',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDatabaseStatus({
|
||||||
|
status: 'error',
|
||||||
|
message: (error as Error).message,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断整体就绪状态
|
||||||
|
const env = get().environment;
|
||||||
|
const hasError =
|
||||||
|
env.messageRouter.status === 'error' ||
|
||||||
|
env.kavaApi.status === 'error' ||
|
||||||
|
env.database.status === 'error';
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
setAppReady('error', '部分服务连接失败,请检查设置');
|
||||||
|
} else {
|
||||||
|
setAppReady('ready');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 辅助函数
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function getOverallStatus(env: EnvironmentCheck): ConnectionStatus {
|
||||||
|
const statuses = [env.messageRouter.status, env.kavaApi.status, env.database.status];
|
||||||
|
|
||||||
|
if (statuses.includes('checking')) return 'checking';
|
||||||
|
if (statuses.includes('error')) return 'error';
|
||||||
|
if (statuses.every((s) => s === 'connected')) return 'connected';
|
||||||
|
if (statuses.includes('disconnected')) return 'disconnected';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusColor(status: ConnectionStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return '#22c55e'; // green
|
||||||
|
case 'checking':
|
||||||
|
return '#f59e0b'; // yellow
|
||||||
|
case 'disconnected':
|
||||||
|
return '#6b7280'; // gray
|
||||||
|
case 'error':
|
||||||
|
return '#ef4444'; // red
|
||||||
|
default:
|
||||||
|
return '#6b7280'; // gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusText(status: ConnectionStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return '已连接';
|
||||||
|
case 'checking':
|
||||||
|
return '检测中...';
|
||||||
|
case 'disconnected':
|
||||||
|
return '未连接';
|
||||||
|
case 'error':
|
||||||
|
return '连接失败';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,10 @@ interface ShareEntry {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastUsedAt?: string;
|
lastUsedAt?: string;
|
||||||
participants: Array<{ partyId: string; name: string }>;
|
participants: Array<{ partyId: string; name: string }>;
|
||||||
|
// 兼容页面组件的 metadata 格式
|
||||||
|
metadata: {
|
||||||
|
participants: Array<{ partyId: string; name: string }>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShareWithRawData extends ShareEntry {
|
interface ShareWithRawData extends ShareEntry {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue