fix: add connection heartbeat and polish mobile tap feedback
This commit is contained in:
parent
207a7d2e33
commit
1a3235be09
@ -41,6 +41,7 @@ export async function mounted() {
|
|||||||
|
|
||||||
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
||||||
this.loadInitialData();
|
this.loadInitialData();
|
||||||
|
this.startConnectionHeartbeat();
|
||||||
|
|
||||||
// 注册全局事件处理器(用于任务轮询)
|
// 注册全局事件处理器(用于任务轮询)
|
||||||
(window as any).__taskEventHandler = (event: any) => {
|
(window as any).__taskEventHandler = (event: any) => {
|
||||||
@ -97,6 +98,7 @@ export function beforeUnmount() {
|
|||||||
this.resourceStopContainerStatsPolling();
|
this.resourceStopContainerStatsPolling();
|
||||||
this.resourceStopProjectStoragePolling();
|
this.resourceStopProjectStoragePolling();
|
||||||
this.resourceStopUsageQuotaPolling();
|
this.resourceStopUsageQuotaPolling();
|
||||||
|
this.stopConnectionHeartbeat();
|
||||||
teardownShowImageObserver();
|
teardownShowImageObserver();
|
||||||
if (this.titleTypingTimer) {
|
if (this.titleTypingTimer) {
|
||||||
clearInterval(this.titleTypingTimer);
|
clearInterval(this.titleTypingTimer);
|
||||||
|
|||||||
@ -1163,6 +1163,50 @@ export const uiMethods = {
|
|||||||
this.socket = null;
|
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() {
|
openRealtimeTerminal() {
|
||||||
const { protocol, hostname, port } = window.location;
|
const { protocol, hostname, port } = window.location;
|
||||||
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
|
const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
|
||||||
|
|||||||
@ -83,6 +83,9 @@ export function dataState() {
|
|||||||
conversationHasVideos: false,
|
conversationHasVideos: false,
|
||||||
conversationListRequestSeq: 0,
|
conversationListRequestSeq: 0,
|
||||||
conversationListRefreshToken: 0,
|
conversationListRefreshToken: 0,
|
||||||
|
connectionHeartbeatTimer: null,
|
||||||
|
connectionHeartbeatFailCount: 0,
|
||||||
|
connectionHeartbeatIntervalMs: 8000,
|
||||||
|
|
||||||
// 工具控制菜单
|
// 工具控制菜单
|
||||||
icons: ICONS,
|
icons: ICONS,
|
||||||
|
|||||||
@ -18,6 +18,13 @@ body[data-theme='dark'] {
|
|||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove iOS/Android WebView default tap highlight */
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Global icon utility */
|
/* Global icon utility */
|
||||||
.icon {
|
.icon {
|
||||||
--icon-size: 1em;
|
--icon-size: 1em;
|
||||||
|
|||||||
@ -246,6 +246,9 @@
|
|||||||
.menu-entry {
|
.menu-entry {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -259,6 +262,11 @@
|
|||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-entry:focus,
|
||||||
|
.menu-entry:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-entry:hover:not(:disabled) {
|
.menu-entry:hover:not(:disabled) {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -162,7 +162,7 @@
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--claude-muted);
|
background: #e34d4d;
|
||||||
transition: background 0.2s ease, box-shadow 0.2s ease;
|
transition: background 0.2s ease, box-shadow 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@ -173,8 +173,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
opacity: 0;
|
background: #e34d4d;
|
||||||
background: transparent;
|
animation: statusPulse 2.4s ease-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-dot.active {
|
.connection-dot.active {
|
||||||
@ -183,7 +183,6 @@
|
|||||||
|
|
||||||
.connection-dot.active::after {
|
.connection-dot.active::after {
|
||||||
background: var(--claude-success);
|
background: var(--claude-success);
|
||||||
animation: statusPulse 2.4s ease-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-icon-enter-active,
|
.mode-icon-enter-active,
|
||||||
|
|||||||
@ -1064,6 +1064,17 @@ class ConversationManager:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
index = self._load_index()
|
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_conversations = len(index)
|
||||||
total_messages = sum(meta.get("total_messages", 0) for meta in index.values())
|
total_messages = sum(meta.get("total_messages", 0) for meta in index.values())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user