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 Layout from './components/Layout';
|
||||
import StartupCheck from './components/StartupCheck';
|
||||
import Home from './pages/Home';
|
||||
import Join from './pages/Join';
|
||||
import Create from './pages/Create';
|
||||
|
|
@ -8,6 +10,13 @@ import Sign from './pages/Sign';
|
|||
import Settings from './pages/Settings';
|
||||
|
||||
function App() {
|
||||
const [startupComplete, setStartupComplete] = useState(false);
|
||||
|
||||
// 显示启动检测页面
|
||||
if (!startupComplete) {
|
||||
return <StartupCheck onComplete={() => setStartupComplete(true)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
|
|
|
|||
|
|
@ -78,31 +78,119 @@
|
|||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.connectionStatus {
|
||||
/* 状态面板 */
|
||||
.statusPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.statusRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: 12px;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.statusLabel {
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.statusValue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--text-disabled);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statusDot[data-status="connected"] {
|
||||
background-color: var(--success-color);
|
||||
.statusText {
|
||||
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"] {
|
||||
background-color: var(--error-color);
|
||||
.operationHeader {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ReactNode } from 'react';
|
||||
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 {
|
||||
|
|
@ -16,6 +17,26 @@ const navItems = [
|
|||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
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 (
|
||||
<div className={styles.layout}>
|
||||
|
|
@ -40,10 +61,108 @@ export default function Layout({ children }: LayoutProps) {
|
|||
))}
|
||||
</ul>
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.connectionStatus}>
|
||||
<span className={styles.statusDot} data-status="connected" />
|
||||
<span>已连接</span>
|
||||
{/* 状态面板 */}
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
lastUsedAt?: string;
|
||||
participants: Array<{ partyId: string; name: string }>;
|
||||
// 兼容页面组件的 metadata 格式
|
||||
metadata: {
|
||||
participants: Array<{ partyId: string; name: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ShareWithRawData extends ShareEntry {
|
||||
|
|
|
|||
Loading…
Reference in New Issue