feat: 添加并优化服务提供商独立测试功能 (#3024)

* feat: 添加并优化服务提供商独立测试功能

* feat: add small size to action buttons in ItemCard and ProviderPage for better UI consistency

---------

Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
RC-CHN
2025-10-13 13:03:20 +08:00
committed by GitHub
parent 79e2743aac
commit 93fcac498c
5 changed files with 116 additions and 50 deletions

View File

@@ -27,7 +27,9 @@
<v-btn
variant="outlined"
color="error"
size="small"
rounded="xl"
:disabled="loading"
@click="$emit('delete', item)"
>
{{ t('core.common.itemCard.delete') }}
@@ -35,7 +37,9 @@
<v-btn
variant="tonal"
color="primary"
size="small"
rounded="xl"
:disabled="loading"
@click="$emit('edit', item)"
>
{{ 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') }}
</v-btn>
<slot name="actions" :item="item"></slot>
<v-spacer></v-spacer>
</v-card-actions>

View File

@@ -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}?"

View File

@@ -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} 吗?"

View File

@@ -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">
<template #actions="{ item }">
<v-btn
style="z-index: 100000;"
variant="tonal"
color="info"
rounded="xl"
size="small"
:loading="isProviderTesting(item.id)"
@click="testSingleProvider(item)"
>
{{ tm('availability.test') }}
</v-btn>
</template>
<template v-slot:details="{ item }">
</template>
</item-card>
@@ -79,7 +93,7 @@
<v-icon class="me-2">mdi-heart-pulse</v-icon>
<span class="text-h4">{{ tm('availability.title') }}</span>
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" :loading="loadingStatus" @click="fetchProviderStatus">
<v-btn color="primary" variant="tonal" :loading="testingProviders.length > 0" @click="fetchProviderStatus">
<v-icon left>mdi-refresh</v-icon>
{{ tm('availability.refresh') }}
</v-btn>
@@ -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;
// 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 };
});
// 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();
}
return axios.get(`/api/config/provider/check_one?id=${p.id}`)
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);
}
}
},

View File

@@ -39,7 +39,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:6185/',
target: 'http://127.0.0.1:6185/',
changeOrigin: true,
}
}