fix: add connection heartbeat and polish mobile tap feedback

This commit is contained in:
JOJO 2026-04-07 00:04:44 +08:00
parent 207a7d2e33
commit 1a3235be09
7 changed files with 78 additions and 4 deletions

View File

@ -41,6 +41,7 @@ export async function mounted() {
// 立即加载初始数据(并行获取状态,优先同步运行模式)
this.loadInitialData();
this.startConnectionHeartbeat();
// 注册全局事件处理器(用于任务轮询)
(window as any).__taskEventHandler = (event: any) => {
@ -97,6 +98,7 @@ export function beforeUnmount() {
this.resourceStopContainerStatsPolling();
this.resourceStopProjectStoragePolling();
this.resourceStopUsageQuotaPolling();
this.stopConnectionHeartbeat();
teardownShowImageObserver();
if (this.titleTypingTimer) {
clearInterval(this.titleTypingTimer);

View File

@ -1163,6 +1163,50 @@ export const uiMethods = {
this.socket = null;
},
async checkConnectionHealth() {
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timeoutId = controller ? window.setTimeout(() => controller.abort(), 5000) : null;
try {
const response = await fetch('/api/status', {
method: 'GET',
cache: 'no-store',
signal: controller?.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
this.isConnected = true;
this.connectionHeartbeatFailCount = 0;
} catch (error) {
this.connectionHeartbeatFailCount = (this.connectionHeartbeatFailCount || 0) + 1;
// 一次失败就先置灰,避免“服务已断但常亮绿色”的误导
this.isConnected = false;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
},
startConnectionHeartbeat() {
if (this.connectionHeartbeatTimer) {
return;
}
this.connectionHeartbeatFailCount = 0;
// 先做一次即时探活,再进入定时轮询
this.checkConnectionHealth();
this.connectionHeartbeatTimer = window.setInterval(() => {
this.checkConnectionHealth();
}, this.connectionHeartbeatIntervalMs || 8000);
},
stopConnectionHeartbeat() {
if (this.connectionHeartbeatTimer) {
clearInterval(this.connectionHeartbeatTimer);
this.connectionHeartbeatTimer = null;
}
},
openRealtimeTerminal() {
const { protocol, hostname, port } = window.location;
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;

View File

@ -83,6 +83,9 @@ export function dataState() {
conversationHasVideos: false,
conversationListRequestSeq: 0,
conversationListRefreshToken: 0,
connectionHeartbeatTimer: null,
connectionHeartbeatFailCount: 0,
connectionHeartbeatIntervalMs: 8000,
// 工具控制菜单
icons: ICONS,

View File

@ -18,6 +18,13 @@ body[data-theme='dark'] {
color: var(--claude-text);
}
/* Remove iOS/Android WebView default tap highlight */
html,
body,
* {
-webkit-tap-highlight-color: transparent;
}
/* Global icon utility */
.icon {
--icon-size: 1em;

View File

@ -246,6 +246,9 @@
.menu-entry {
border: none;
background: transparent;
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
padding: 10px 12px;
border-radius: 12px;
font-size: 14px;
@ -259,6 +262,11 @@
min-height: 44px;
}
.menu-entry:focus,
.menu-entry:focus-visible {
outline: none;
}
.menu-entry:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.05);
}

View File

@ -162,7 +162,7 @@
width: 12px;
height: 12px;
border-radius: 6px;
background: var(--claude-muted);
background: #e34d4d;
transition: background 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: visible;
@ -173,8 +173,8 @@
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
background: transparent;
background: #e34d4d;
animation: statusPulse 2.4s ease-out infinite;
}
.connection-dot.active {
@ -183,7 +183,6 @@
.connection-dot.active::after {
background: var(--claude-success);
animation: statusPulse 2.4s ease-out infinite;
}
.mode-icon-enter-active,

View File

@ -1064,6 +1064,17 @@ class ConversationManager:
"""
try:
index = self._load_index()
# 统计必须基于全量对话避免启动阶段“仅预热最近20条索引”导致总数偏小。
# 这里按需触发一次全量重建;后续统计可复用已保存的完整索引。
try:
total_files = len(self._iter_conversation_files(sort_by_mtime=False))
except Exception:
total_files = 0
if total_files and len(index) < total_files:
rebuilt_full = self._rebuild_index_from_files(max_count=None)
if rebuilt_full:
self._save_index(rebuilt_full)
index = rebuilt_full
total_conversations = len(index)
total_messages = sum(meta.get("total_messages", 0) for meta in index.values())