Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dashboard/src/components/shared/ItemCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,19 @@
<v-btn
variant="outlined"
color="error"
size="small"
rounded="xl"
:disabled="loading"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: 基于加载状态禁用按钮可能会阻塞不相关的操作。

审查在加载期间真正需要禁用的操作;像复制这样的操作可能不需要被阻塞,以获得更好的用户体验。

Original comment in English

question: Disabling buttons based on loading state may block unrelated actions.

Review which actions truly need to be disabled during loading; actions like copy may not need to be blocked for a better user experience.

@click="$emit('delete', item)"
>
{{ t('core.common.itemCard.delete') }}
</v-btn>
<v-btn
variant="tonal"
color="primary"
size="small"
rounded="xl"
:disabled="loading"
@click="$emit('edit', item)"
>
{{ t('core.common.itemCard.edit') }}
Expand All @@ -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>

Expand Down
6 changes: 4 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"available": "Available",
"unavailable": "Unavailable",
"pending": "Pending...",
"errorMessage": "Error Message"
"errorMessage": "Error Message",
"test": "Test"
},
"logs": {
"title": "Service Logs",
Expand Down Expand Up @@ -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}?"
Expand Down
6 changes: 4 additions & 2 deletions dashboard/src/i18n/locales/zh-CN/features/provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"available": "可用",
"unavailable": "不可用",
"pending": "检查中...",
"errorMessage": "错误信息"
"errorMessage": "错误信息",
"test": "测试"
},
"logs": {
"title": "服务日志",
Expand Down Expand Up @@ -77,7 +78,8 @@
},
"error": {
"sessionSeparation": "获取会话隔离配置失败",
"fetchStatus": "获取服务提供商状态失败"
"fetchStatus": "获取服务提供商状态失败",
"testError": "测试 {id} 失败: {error}"
},
"confirm": {
"delete": "确定要删除服务提供商 {id} 吗?"
Expand Down
145 changes: 100 additions & 45 deletions dashboard/src/views/ProviderPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down Expand Up @@ -288,7 +302,7 @@ export default {

// 供应商状态相关
providerStatuses: [],
loadingStatus: false,
testingProviders: [], // 存储正在测试的 provider ID

// 新增提供商对话框相关
showAddProviderDialog: false,
Expand Down Expand Up @@ -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')
Expand All @@ -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')
}
};
},
Expand Down Expand Up @@ -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);
}
}
},

Expand Down
2 changes: 1 addition & 1 deletion dashboard/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:6185/',
target: 'http://127.0.0.1:6185/',
changeOrigin: true,
}
}
Expand Down