From f5cbc855f6aa19dd3b93e3ab3dd685524d3ce255 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 28 Dec 2025 20:33:00 -0800 Subject: [PATCH] =?UTF-8?q?feat(service-party-app):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=8A=B6=E6=80=81=E6=A3=80=E6=B5=8B=E5=92=8C?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 新增功能 ### 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 --- .../services/service-party-app/src/App.tsx | 9 + .../src/components/Layout.module.css | 112 +++++++- .../src/components/Layout.tsx | 127 ++++++++- .../src/components/StartupCheck.module.css | 171 ++++++++++++ .../src/components/StartupCheck.tsx | 172 ++++++++++++ .../service-party-app/src/stores/appStore.ts | 263 ++++++++++++++++++ .../service-party-app/src/types/electron.d.ts | 4 + 7 files changed, 842 insertions(+), 16 deletions(-) create mode 100644 backend/mpc-system/services/service-party-app/src/components/StartupCheck.module.css create mode 100644 backend/mpc-system/services/service-party-app/src/components/StartupCheck.tsx create mode 100644 backend/mpc-system/services/service-party-app/src/stores/appStore.ts diff --git a/backend/mpc-system/services/service-party-app/src/App.tsx b/backend/mpc-system/services/service-party-app/src/App.tsx index f7987ada..c12ed81d 100644 --- a/backend/mpc-system/services/service-party-app/src/App.tsx +++ b/backend/mpc-system/services/service-party-app/src/App.tsx @@ -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 setStartupComplete(true)} />; + } + return ( diff --git a/backend/mpc-system/services/service-party-app/src/components/Layout.module.css b/backend/mpc-system/services/service-party-app/src/components/Layout.module.css index 09e289f6..34667860 100644 --- a/backend/mpc-system/services/service-party-app/src/components/Layout.module.css +++ b/backend/mpc-system/services/service-party-app/src/components/Layout.module.css @@ -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 { diff --git a/backend/mpc-system/services/service-party-app/src/components/Layout.tsx b/backend/mpc-system/services/service-party-app/src/components/Layout.tsx index 07783bb9..6221b3fb 100644 --- a/backend/mpc-system/services/service-party-app/src/components/Layout.tsx +++ b/backend/mpc-system/services/service-party-app/src/components/Layout.tsx @@ -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 (
@@ -40,10 +61,108 @@ export default function Layout({ children }: LayoutProps) { ))}
-
- - 已连接 + {/* 状态面板 */} +
+ {/* 消息路由 */} +
+ 消息路由 + + + + {environment.messageRouter.status === 'connected' + ? '已连接' + : environment.messageRouter.status === 'checking' + ? '检测中...' + : environment.messageRouter.status === 'error' + ? '失败' + : '未连接'} + + +
+ + {/* Kava API */} +
+ Kava API + + + + {environment.kavaApi.status === 'connected' + ? '已连接' + : environment.kavaApi.status === 'checking' + ? '检测中...' + : environment.kavaApi.status === 'error' + ? '失败' + : '未连接'} + + +
+ + {/* 本地存储 */} +
+ 本地存储 + + + + {environment.database.status === 'connected' + ? environment.database.message + : environment.database.status === 'checking' + ? '检测中...' + : environment.database.status === 'error' + ? '错误' + : '未知'} + + +
+ + {/* 刷新按钮 */} +
+ + {appReady === 'ready' ? '✅ 就绪' : appReady === 'error' ? '⚠️ 部分异常' : '🔄 初始化中'} + + +
+ + {/* 操作进度面板 */} + {(operation.status === 'connecting' || operation.status === 'in_progress') && ( +
+
+ + {getOperationTitle()} +
+ {operation.currentStep !== undefined && operation.totalSteps !== undefined && ( + <> +
+
+
+
+ {operation.stepDescription || `步骤 ${operation.currentStep}/${operation.totalSteps}`} +
+ + )} +
+ )}
{children}
diff --git a/backend/mpc-system/services/service-party-app/src/components/StartupCheck.module.css b/backend/mpc-system/services/service-party-app/src/components/StartupCheck.module.css new file mode 100644 index 00000000..bf47d914 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/components/StartupCheck.module.css @@ -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); +} diff --git a/backend/mpc-system/services/service-party-app/src/components/StartupCheck.tsx b/backend/mpc-system/services/service-party-app/src/components/StartupCheck.tsx new file mode 100644 index 00000000..230110fa --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/components/StartupCheck.tsx @@ -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 ( +
+
+ {/* Logo */} +
+ 🍈 +

绿积分共管账户服务

+

榴莲皇后 · 分布式多方共管钱包

+
+ + {/* 状态检测 */} +
+
+ {getStatusIcon(environment.database.status)} + 本地数据库 + + {environment.database.status === 'connected' + ? '正常' + : environment.database.status === 'checking' + ? '检测中...' + : environment.database.status === 'error' + ? '异常' + : '等待检测'} + +
+ +
+ {getStatusIcon(environment.messageRouter.status)} + 消息路由服务 + + {environment.messageRouter.status === 'connected' + ? '已连接' + : environment.messageRouter.status === 'checking' + ? '检测中...' + : environment.messageRouter.status === 'error' + ? '连接失败' + : '等待检测'} + +
+ +
+ {getStatusIcon(environment.kavaApi.status)} + Kava 区块链 API + + {environment.kavaApi.status === 'connected' + ? '已连接' + : environment.kavaApi.status === 'checking' + ? '检测中...' + : environment.kavaApi.status === 'error' + ? '连接失败' + : '等待检测'} + +
+
+ + {/* 总体状态 */} +
+ {appReady === 'initializing' && ( + <> +
+ 正在初始化环境... + + )} + {appReady === 'ready' && ( + <> + + 环境检测完成,即将进入主界面 + + )} + {appReady === 'error' && ( + <> + ⚠️ + 部分服务异常,但仍可使用基本功能 + + )} +
+ + {/* 详情按钮 */} + {(environment.messageRouter.message || environment.kavaApi.message) && ( + + )} + + {/* 详细信息 */} + {showDetails && ( +
+ {environment.messageRouter.message && ( +
+ 消息路由: + {environment.messageRouter.message} +
+ )} + {environment.kavaApi.message && ( +
+ Kava API: + {environment.kavaApi.message} +
+ )} + {environment.database.message && ( +
+ 本地存储: + {environment.database.message} +
+ )} +
+ )} + + {/* 手动进入按钮 */} + {(appReady === 'ready' || appReady === 'error') && ( + + )} +
+
+ ); +} diff --git a/backend/mpc-system/services/service-party-app/src/stores/appStore.ts b/backend/mpc-system/services/service-party-app/src/stores/appStore.ts new file mode 100644 index 00000000..06a9bb86 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/stores/appStore.ts @@ -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) => void; + resetOperation: () => void; + checkAllServices: () => Promise; +} + +// ============================================================================= +// Store +// ============================================================================= + +export const useAppStore = create((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 '未知'; + } +} diff --git a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts index c81bb59e..d0c6eba9 100644 --- a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts +++ b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts @@ -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 {