('checking')
+
+ useEffect(() => {
+ const controller = new AbortController()
+ const timer = window.setTimeout(() => void checkConnection(controller.signal), 300)
+ return () => {
+ controller.abort()
+ window.clearTimeout(timer)
+ }
+ }, [server])
+
+ async function checkConnection(signal?: AbortSignal) {
+ setConnection('checking')
+ try {
+ const online = await checkServerConnection(server, signal)
+ if (!signal?.aborted) setConnection(online ? 'online' : 'offline')
+ } catch (requestError) {
+ if (!(requestError instanceof DOMException && requestError.name === 'AbortError')) setConnection('offline')
+ }
+ }
async function submitLogin() {
if (loading) return
@@ -332,6 +353,11 @@ function Login({ onLogin }: { onLogin: (session: ApiSession) => void }) {
森林AI Mini
停靠在屏幕一侧,随时记录与推进项目
+
+
+ {connection === 'checking' ? '正在检查' : connection === 'online' ? '连接正常' : '连接异常'}
+
+
{error ? : null}
diff --git a/apps/mini/src/api.ts b/apps/mini/src/api.ts
index 5476512..c851320 100644
--- a/apps/mini/src/api.ts
+++ b/apps/mini/src/api.ts
@@ -94,6 +94,18 @@ export async function login(server: string, email: string, password: string): Pr
return { baseUrl, token: response.token }
}
+export async function checkServerConnection(server: string, signal?: AbortSignal) {
+ try {
+ const response = await fetch(`${normalizeBaseUrl(server)}/api/v1/status`, { signal })
+ if (!response.ok) return false
+ const payload = await response.json() as { timestamp?: unknown }
+ return typeof payload.timestamp === 'string'
+ } catch (error) {
+ if (error instanceof DOMException && error.name === 'AbortError') throw error
+ return false
+ }
+}
+
export async function fetchProjects(session: ApiSession) {
useSessionBase(session)
return request('/api/v1/projects', { token: session.token })
diff --git a/apps/mini/src/styles.css b/apps/mini/src/styles.css
index 9a7c7d1..31bcda8 100644
--- a/apps/mini/src/styles.css
+++ b/apps/mini/src/styles.css
@@ -224,6 +224,12 @@ html[data-dock="left"] .mini-shell { margin-right: auto; }
.login-note label, .mini-form label { display: grid; gap: 5px; color: #4e5969; font-size: 11px; }
.login-note .arco-input-wrapper, .mini-form .arco-input-wrapper { border-radius: 9px; background: rgba(255,255,255,.82); }
.login-note > small { color: var(--muted); text-align: center; }
+.mini-connection { display: grid; grid-template-columns: 9px 1fr auto; align-items: center; gap: 8px; margin-top: -5px; border: 1px solid #bedaff; border-radius: 9px; background: #f2f8ff; padding: 8px 10px; color: #165dff; font-size: 11px; }
+.mini-connection > i { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
+.mini-connection > strong { font-weight: 600; }
+.mini-connection > button { border: 0; background: transparent; padding: 0; color: inherit; cursor: pointer; font-size: 11px; }
+.mini-connection.online { border-color: #7be188; background: #f0fff4; color: #168a2f; }
+.mini-connection.offline { border-color: #f53f3f; background: #fff2f0; color: #d91d35; }
.mini-modal .arco-modal { width: min(380px, calc(100vw - 24px)); }
.mini-modal .arco-modal-content { border-radius: 15px; }
diff --git a/apps/web_v1/src/App.css b/apps/web_v1/src/App.css
index 8aa5158..91edaa3 100644
--- a/apps/web_v1/src/App.css
+++ b/apps/web_v1/src/App.css
@@ -191,6 +191,26 @@
font-weight: 600;
}
+.connection-card.checking {
+ border-color: #bedaff;
+ background: #f2f8ff;
+ color: #165dff;
+}
+
+.connection-card.checking .connection-title {
+ color: #165dff;
+}
+
+.connection-card.offline {
+ border-color: #f53f3f;
+ background: #fff2f0;
+ color: #d91d35;
+}
+
+.connection-card.offline .connection-title {
+ color: #d91d35;
+}
+
.login-value-panel {
display: grid;
align-content: center;