diff --git a/dashboard/src/components/shared/ItemCard.vue b/dashboard/src/components/shared/ItemCard.vue index 14453ae7a..0b7c56a59 100644 --- a/dashboard/src/components/shared/ItemCard.vue +++ b/dashboard/src/components/shared/ItemCard.vue @@ -27,7 +27,9 @@ {{ t('core.common.itemCard.delete') }} @@ -35,7 +37,9 @@ {{ t('core.common.itemCard.edit') }} @@ -44,11 +48,14 @@ v-if="showCopyButton" variant="tonal" color="secondary" + size="small" rounded="xl" + :disabled="loading" @click="$emit('copy', item)" > {{ t('core.common.itemCard.copy') }} + diff --git a/dashboard/src/i18n/locales/en-US/features/provider.json b/dashboard/src/i18n/locales/en-US/features/provider.json index ad32bde5d..7888c9e0b 100644 --- a/dashboard/src/i18n/locales/en-US/features/provider.json +++ b/dashboard/src/i18n/locales/en-US/features/provider.json @@ -31,7 +31,8 @@ "available": "Available", "unavailable": "Unavailable", "pending": "Pending...", - "errorMessage": "Error Message" + "errorMessage": "Error Message", + "test": "Test" }, "logs": { "title": "Service Logs", @@ -76,7 +77,8 @@ }, "error": { "sessionSeparation": "Failed to get session isolation configuration", - "fetchStatus": "Failed to get service provider status" + "fetchStatus": "Failed to get service provider status", + "testError": "Test failed for {id}: {error}" }, "confirm": { "delete": "Are you sure you want to delete service provider {id}?" diff --git a/dashboard/src/i18n/locales/zh-CN/features/provider.json b/dashboard/src/i18n/locales/zh-CN/features/provider.json index d82deed5a..c1016f061 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/provider.json +++ b/dashboard/src/i18n/locales/zh-CN/features/provider.json @@ -32,7 +32,8 @@ "available": "可用", "unavailable": "不可用", "pending": "检查中...", - "errorMessage": "错误信息" + "errorMessage": "错误信息", + "test": "测试" }, "logs": { "title": "服务日志", @@ -77,7 +78,8 @@ }, "error": { "sessionSeparation": "获取会话隔离配置失败", - "fetchStatus": "获取服务提供商状态失败" + "fetchStatus": "获取服务提供商状态失败", + "testError": "测试 {id} 失败: {error}" }, "confirm": { "delete": "确定要删除服务提供商 {id} 吗?" diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index 4aa012cf1..13fbc2d7e 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -60,12 +60,26 @@ :item="provider" title-field="id" enabled-field="enable" + :loading="isProviderTesting(provider.id)" @toggle-enabled="providerStatusChange" :bglogo="getProviderIcon(provider.provider)" @delete="deleteProvider" @edit="configExistingProvider" @copy="copyProvider" :show-copy-button="true"> + @@ -79,7 +93,7 @@ mdi-heart-pulse {{ tm('availability.title') }} - + mdi-refresh {{ tm('availability.refresh') }} @@ -288,7 +302,7 @@ export default { // 供应商状态相关 providerStatuses: [], - loadingStatus: false, + testingProviders: [], // 存储正在测试的 provider ID // 新增提供商对话框相关 showAddProviderDialog: false, @@ -359,7 +373,8 @@ export default { statusUpdate: this.tm('messages.success.statusUpdate'), }, error: { - fetchStatus: this.tm('messages.error.fetchStatus') + fetchStatus: this.tm('messages.error.fetchStatus'), + testError: this.tm('messages.error.testError') }, confirm: { delete: this.tm('messages.confirm.delete') @@ -368,6 +383,9 @@ export default { available: this.tm('availability.available'), unavailable: this.tm('availability.unavailable'), pending: this.tm('availability.pending') + }, + availability: { + test: this.tm('availability.test') } }; }, @@ -615,70 +633,107 @@ export default { // 获取供应商状态 async fetchProviderStatus() { - if (this.loadingStatus) return; + if (this.testingProviders.length > 0) return; - this.loadingStatus = true; this.showStatus = true; // 自动展开状态部分 - // 1. 立即初始化UI为pending状态 - this.providerStatuses = this.config_data.provider.map(p => ({ - id: p.id, - name: p.id, - status: 'pending', - error: null - })); + const providersToTest = this.config_data.provider.filter(p => p.enable); + if (providersToTest.length === 0) return; - // 2. 为每个provider创建一个并发的测试请求 - const promises = this.config_data.provider.map(p => { - if (!p.enable) { - const index = this.providerStatuses.findIndex(s => s.id === p.id); - if (index !== -1) { - const disabledStatus = { - ...this.providerStatuses[index], - status: 'unavailable', - error: '该提供商未被用户启用' - }; - this.providerStatuses.splice(index, 1, disabledStatus); - } - return Promise.resolve(); - } + // 1. 初始化UI为pending状态,并将所有待测试的 provider ID 加入 loading 列表 + this.providerStatuses = providersToTest.map(p => { + this.testingProviders.push(p.id); + return { id: p.id, name: p.id, status: 'pending', error: null }; + }); - return axios.get(`/api/config/provider/check_one?id=${p.id}`) + // 2. 为每个provider创建一个并发的测试请求 + const promises = providersToTest.map(p => + axios.get(`/api/config/provider/check_one?id=${p.id}`) .then(res => { if (res.data && res.data.status === 'ok') { - // 成功,更新对应的provider状态 const index = this.providerStatuses.findIndex(s => s.id === p.id); - if (index !== -1) { - this.providerStatuses.splice(index, 1, res.data.data); - } + if (index !== -1) this.providerStatuses.splice(index, 1, res.data.data); } else { - // 接口返回了业务错误 throw new Error(res.data?.message || `Failed to check status for ${p.id}`); } }) .catch(err => { - // 网络错误或业务错误 const errorMessage = err.response?.data?.message || err.message || 'Unknown error'; const index = this.providerStatuses.findIndex(s => s.id === p.id); if (index !== -1) { - const failedStatus = { - ...this.providerStatuses[index], - status: 'unavailable', - error: errorMessage - }; + const failedStatus = { ...this.providerStatuses[index], status: 'unavailable', error: errorMessage }; this.providerStatuses.splice(index, 1, failedStatus); } - // 可以在这里选择性地向上抛出错误,以便Promise.allSettled知道 - return Promise.reject(errorMessage); - }); - }); + return Promise.reject(errorMessage); // Propagate error for Promise.allSettled + }) + ); - // 3. 等待所有请求完成(无论成功或失败) + // 3. 等待所有请求完成 try { await Promise.allSettled(promises); } finally { - // 4. 关闭全局加载状态 - this.loadingStatus = false; + // 4. 关闭所有加载状态 + this.testingProviders = []; + } + }, + + isProviderTesting(providerId) { + return this.testingProviders.includes(providerId); + }, + + async testSingleProvider(provider) { + if (this.isProviderTesting(provider.id)) return; + + this.testingProviders.push(provider.id); + this.showStatus = true; // 自动展开状态部分 + + // 更新UI为pending状态 + const statusIndex = this.providerStatuses.findIndex(s => s.id === provider.id); + const pendingStatus = { + id: provider.id, + name: provider.id, + status: 'pending', + error: null + }; + if (statusIndex !== -1) { + this.providerStatuses.splice(statusIndex, 1, pendingStatus); + } else { + this.providerStatuses.unshift(pendingStatus); + } + + try { + if (!provider.enable) { + throw new Error('该提供商未被用户启用'); + } + + const res = await axios.get(`/api/config/provider/check_one?id=${provider.id}`); + if (res.data && res.data.status === 'ok') { + const index = this.providerStatuses.findIndex(s => s.id === provider.id); + if (index !== -1) { + this.providerStatuses.splice(index, 1, res.data.data); + } + } else { + throw new Error(res.data?.message || `Failed to check status for ${provider.id}`); + } + } catch (err) { + const errorMessage = err.response?.data?.message || err.message || 'Unknown error'; + const index = this.providerStatuses.findIndex(s => s.id === provider.id); + const failedStatus = { + id: provider.id, + name: provider.id, + status: 'unavailable', + error: errorMessage + }; + if (index !== -1) { + this.providerStatuses.splice(index, 1, failedStatus); + } + // 不再显示全局的错误提示,因为卡片本身会显示错误信息 + // this.showError(this.tm('messages.error.testError', { id: provider.id, error: errorMessage })); + } finally { + const index = this.testingProviders.indexOf(provider.id); + if (index > -1) { + this.testingProviders.splice(index, 1); + } } }, diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 673187ded..b52ad65b6 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -39,7 +39,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:6185/', + target: 'http://127.0.0.1:6185/', changeOrigin: true, } }