From cb1b9ba88930018d00efabb17e96dde454f90af9 Mon Sep 17 00:00:00 2001 From: Kris <2893855659@qq.com> Date: Thu, 14 May 2026 09:47:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=85=A8=E9=87=8F=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=B8=8E=E6=97=A5=E5=BF=97=E8=AF=8A=E6=96=AD=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构api请求统一处理接口返回错误,标准化错误抛出格式 2. 新增渠道类型常量与图标映射,清理冗余图标配置 3. 重构凭证状态逻辑,抽离为可复用composable工具 4. 优化WebSocket连接管理,新增断开状态标记防止重连异常 5. 新增渠道运行日志组件,支持实时查看与刷新日志 6. 新增渠道诊断报告功能,一键生成并复制渠道状态报告 7. 优化消息日志面板,支持正则搜索、多格式导出与消息重发 8. 新增统计图表对比功能,支持多渠道数据横向对比 9. 优化仪表盘统计字段映射,修正today_count与total_count字段名 10. 重构渠道启停逻辑,添加异常回滚与错误捕获 11. 新增批量注销渠道功能,优化配置保存后的重启提示 12. 优化轮询机制,新增页面可见性监听与指数退避重试策略 --- web/src/apis/base.js | 9 +- web/src/apis/channel_api.js | 4 + .../channels/ChannelAbilityPanel.vue | 66 ++- .../components/channels/ChannelDebugLog.vue | 334 ++++++++++++++ .../components/channels/ChannelMessageLog.vue | 202 +++++++-- .../components/channels/ChannelStatsPanel.vue | 292 ++++++++++++- .../components/channels/ChannelStatusCard.vue | 42 +- web/src/composables/useCredentialStatus.js | 57 +++ web/src/constants/channelIcons.js | 23 - web/src/constants/channelTypes.js | 27 ++ web/src/stores/channel.js | 92 +++- web/src/views/ChannelManageView.vue | 412 +++++++++++++++--- web/src/views/DashboardView.vue | 4 +- 13 files changed, 1364 insertions(+), 200 deletions(-) create mode 100644 web/src/components/channels/ChannelDebugLog.vue create mode 100644 web/src/composables/useCredentialStatus.js create mode 100644 web/src/constants/channelTypes.js 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) {