diff --git a/web/src/apis/base.js b/web/src/apis/base.js index b4f369dd..09f73a07 100644 --- a/web/src/apis/base.js +++ b/web/src/apis/base.js @@ -123,7 +123,14 @@ export async function apiRequest(url, options = {}, requiresAuth = true, respons // 检查Content-Type以确定如何处理响应 const contentType = response.headers.get('Content-Type') if (contentType && contentType.includes('application/json')) { - return await response.json() + const data = await response.json() + if (data && typeof data.code === 'number' && data.code !== 0) { + const error = new Error(data.message || '请求失败') + error.code = data.code + error.data = data.data + throw error + } + return data } return await response.text() } else if (responseType === 'text') { diff --git a/web/src/apis/channel_api.js b/web/src/apis/channel_api.js index 8088505a..6157ee76 100644 --- a/web/src/apis/channel_api.js +++ b/web/src/apis/channel_api.js @@ -11,6 +11,7 @@ class ChannelWSClient { this.heartbeatInterval = heartbeatInterval this.heartbeatTimer = null this.pendingQueue = [] + this._disconnecting = false this.eventHandlers = { onChunk: null, onDone: null, @@ -22,6 +23,7 @@ class ChannelWSClient { } connect() { + this._disconnecting = false return new Promise((resolve, reject) => { this.ws = new WebSocket(`${this.url}?token=${this.token}`) @@ -41,6 +43,7 @@ class ChannelWSClient { this.ws.onclose = (event) => { this._stopHeartbeat() this._emitStateChange('disconnected') + if (this._disconnecting) return if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) { this._scheduleReconnect() } @@ -71,6 +74,7 @@ class ChannelWSClient { } disconnect() { + this._disconnecting = true this.maxReconnectAttempts = 0 this._stopHeartbeat() this.ws?.close(1000, 'Client disconnect') diff --git a/web/src/components/channels/ChannelAbilityPanel.vue b/web/src/components/channels/ChannelAbilityPanel.vue index 89821d82..faa15c88 100644 --- a/web/src/components/channels/ChannelAbilityPanel.vue +++ b/web/src/components/channels/ChannelAbilityPanel.vue @@ -1,6 +1,6 @@ + + + + \ No newline at end of file diff --git a/web/src/components/channels/ChannelMessageLog.vue b/web/src/components/channels/ChannelMessageLog.vue index be6f0226..334a139f 100644 --- a/web/src/components/channels/ChannelMessageLog.vue +++ b/web/src/components/channels/ChannelMessageLog.vue @@ -1,14 +1,17 @@ - @@ -1618,4 +1775,127 @@ onUnmounted(() => { &.cred-none { background: var(--gray-100); color: var(--gray-500); } &.cred-memory { background: var(--color-warning-50); color: var(--color-warning-700); } } + +.restart-hint { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 14px; + margin-bottom: 16px; + border-radius: 6px; + background: var(--color-warning-50); + border: 1px solid var(--color-warning-200); + font-size: 13px; + color: var(--color-warning-700); +} + +.diagnostic-title-icon { + vertical-align: -3px; + margin-right: 4px; + color: var(--main-color); +} + +.diagnostic-content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.diagnostic-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + + @media (max-width: 767px) { + grid-template-columns: 1fr; + } +} + +.diagnostic-meta-item { + display: flex; + flex-direction: column; + gap: 2px; + + .diagnostic-meta-label { + font-size: 12px; + color: var(--gray-600); + font-weight: 500; + } + + .diagnostic-meta-value { + font-size: 14px; + color: var(--gray-1000); + + &.mono { + font-family: monospace; + font-size: 13px; + } + } +} + +.diagnostic-checks { + display: flex; + flex-direction: column; + gap: 8px; + border-top: 1px solid var(--gray-150); + padding-top: 12px; +} + +.diagnostic-check-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + border-radius: 6px; + background: var(--gray-25); + + &.diagnostic-check--pass { + border-left: 3px solid var(--color-success-500); + } + + &.diagnostic-check--warn { + border-left: 3px solid var(--color-warning-500); + background: var(--color-warning-50); + } + + &.diagnostic-check--fail { + border-left: 3px solid var(--color-error-500); + background: var(--color-error-50); + } + + &.diagnostic-check--info { + border-left: 3px solid var(--color-info-500); + } +} + +.diagnostic-check-icon { + font-size: 16px; + line-height: 1.4; + flex-shrink: 0; +} + +.diagnostic-check-body { + display: flex; + flex-direction: column; + gap: 2px; +} + +.diagnostic-check-name { + font-size: 14px; + font-weight: 600; + color: var(--gray-1000); +} + +.diagnostic-check-detail { + font-size: 12px; + color: var(--gray-600); +} + +.diagnostic-empty { + text-align: center; + padding: 32px; + color: var(--gray-500); + font-size: 14px; +} \ No newline at end of file diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 602bd467..443ce37f 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -177,9 +177,9 @@ async function loadChannelStats() { const total = channels.length const connected = channels.filter((c) => c.status === 'connected').length const errorCount = channels.filter((c) => c.status === 'error').length - const todayMessages = channels.reduce((sum, c) => sum + (c.today_count || 0), 0) + const todayMessages = channels.reduce((sum, c) => sum + (c.today_messages || 0), 0) const totalSuccess = channels.reduce((sum, c) => sum + (c.success_count || 0), 0) - const totalAll = channels.reduce((sum, c) => sum + (c.total_count || 0), 0) + const totalAll = channels.reduce((sum, c) => sum + (c.total_messages || 0), 0) const successRate = totalAll > 0 ? Math.round((totalSuccess / totalAll) * 100) : 0 channelStats.value = { total, connected, error: errorCount, todayMessages, successRate } } catch (e) {