2025-12-10 20:52:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="space-y-4">
|
2026-03-12 01:11:35 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
v-if="providerDeleteProgress"
|
|
|
|
|
|
class="border-primary/30 bg-primary/5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="px-5 py-4 space-y-4">
|
|
|
|
|
|
<div class="flex items-start justify-between gap-4">
|
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
|
<div class="text-sm font-semibold text-foreground">
|
|
|
|
|
|
正在删除提供商:{{ providerDeleteProgress.providerName }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ providerDeleteStageLabel }} · {{ providerDeleteProgress.message || '后台处理中' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="shrink-0 text-right">
|
|
|
|
|
|
<div class="text-xs font-medium text-primary">
|
|
|
|
|
|
{{ providerDeleteOverallPercent }}%
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-[11px] text-muted-foreground">
|
|
|
|
|
|
{{ providerDeleteCompletedUnits }}/{{ providerDeleteTotalUnits }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
|
|
|
|
|
<span>总体进度</span>
|
|
|
|
|
|
<span>{{ providerDeleteCompletedUnits }}/{{ providerDeleteTotalUnits }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="h-2 rounded-full bg-primary/10 overflow-hidden">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="h-full bg-primary transition-all duration-300"
|
|
|
|
|
|
:style="{ width: `${providerDeleteOverallPercent}%` }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
|
|
|
|
|
<span>账号删除</span>
|
|
|
|
|
|
<span>{{ providerDeleteProgress.deletedKeys }}/{{ providerDeleteProgress.totalKeys || '...' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="h-2 rounded-full bg-primary/10 overflow-hidden">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="h-full bg-primary/80 transition-all duration-300"
|
|
|
|
|
|
:style="{ width: `${providerDeleteKeysPercent}%` }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
|
|
|
|
|
<span>端点删除</span>
|
|
|
|
|
|
<span>{{ providerDeleteProgress.deletedEndpoints }}/{{ providerDeleteProgress.totalEndpoints || '...' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="h-2 rounded-full bg-primary/10 overflow-hidden">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="h-full bg-primary/60 transition-all duration-300"
|
|
|
|
|
|
:style="{ width: `${providerDeleteEndpointsPercent}%` }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<!-- 提供商表格 -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<!-- 标题和操作栏 -->
|
2026-02-14 20:06:38 +08:00
|
|
|
|
<ProviderTableHeader
|
|
|
|
|
|
:search-query="searchQuery"
|
|
|
|
|
|
:filter-status="filterStatus"
|
|
|
|
|
|
:filter-api-format="filterApiFormat"
|
|
|
|
|
|
:filter-model="filterModel"
|
|
|
|
|
|
:status-filters="statusFilters"
|
|
|
|
|
|
:api-format-filters="apiFormatFilters"
|
|
|
|
|
|
:model-filters="modelFilters"
|
|
|
|
|
|
:has-active-filters="hasActiveFilters"
|
|
|
|
|
|
:priority-mode-label="priorityModeConfig.label"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
@update:search-query="searchQuery = $event"
|
|
|
|
|
|
@update:filter-status="filterStatus = $event"
|
|
|
|
|
|
@update:filter-api-format="filterApiFormat = $event"
|
|
|
|
|
|
@update:filter-model="filterModel = $event"
|
|
|
|
|
|
@reset-filters="resetFilters"
|
|
|
|
|
|
@open-priority-dialog="openPriorityDialog"
|
|
|
|
|
|
@add-provider="openAddProviderDialog"
|
|
|
|
|
|
@refresh="loadProviders"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 加载状态 -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="loading"
|
|
|
|
|
|
class="flex items-center justify-center py-12"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<div
|
2026-03-09 12:45:54 +08:00
|
|
|
|
v-else-if="providers.length === 0"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
class="flex flex-col items-center justify-center py-16 text-center"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div class="text-muted-foreground mb-2">
|
2026-02-06 21:52:22 +08:00
|
|
|
|
<template v-if="hasActiveFilters">
|
|
|
|
|
|
未找到匹配当前筛选条件的提供商
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
暂无提供商,点击右上角添加
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<Button
|
2026-02-06 21:52:22 +08:00
|
|
|
|
v-if="hasActiveFilters"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
2026-02-14 20:06:38 +08:00
|
|
|
|
@click="resetFilters"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
>
|
2026-02-06 21:52:22 +08:00
|
|
|
|
清除筛选
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 桌面端表格 -->
|
2025-12-12 16:15:54 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
2025-12-13 22:26:47 +08:00
|
|
|
|
class="hidden xl:block overflow-x-auto"
|
2025-12-12 16:15:54 +08:00
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
2026-02-03 18:48:39 +08:00
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead class="w-[18%] min-w-[140px]">
|
2025-12-12 16:15:54 +08:00
|
|
|
|
提供商信息
|
|
|
|
|
|
</TableHead>
|
2026-02-03 18:48:39 +08:00
|
|
|
|
<TableHead class="w-[20%] min-w-[180px]">
|
2026-01-17 19:50:35 +08:00
|
|
|
|
余额监控
|
2025-12-12 16:15:54 +08:00
|
|
|
|
</TableHead>
|
2026-05-01 02:14:49 +08:00
|
|
|
|
<SortableTableHead
|
|
|
|
|
|
class="w-[12%] min-w-[100px] text-center"
|
|
|
|
|
|
column-key="model"
|
|
|
|
|
|
:sortable="false"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
:filter-active="filterModel !== 'all'"
|
|
|
|
|
|
filter-title="筛选模型"
|
|
|
|
|
|
filter-content-class="w-64 p-1 rounded-2xl border-border bg-card text-foreground shadow-2xl backdrop-blur-xl"
|
|
|
|
|
|
>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
资源统计
|
2026-05-01 02:14:49 +08:00
|
|
|
|
<template #filter="{ close }">
|
|
|
|
|
|
<TableFilterMenu
|
|
|
|
|
|
v-model="filterModel"
|
|
|
|
|
|
:options="modelFilters"
|
|
|
|
|
|
@select="close"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</SortableTableHead>
|
|
|
|
|
|
<SortableTableHead
|
|
|
|
|
|
class="w-[24%] min-w-[260px]"
|
|
|
|
|
|
column-key="api_format"
|
|
|
|
|
|
:sortable="false"
|
|
|
|
|
|
:filter-active="filterApiFormat !== 'all'"
|
|
|
|
|
|
filter-title="筛选 API 格式"
|
|
|
|
|
|
filter-content-class="w-72 p-1 rounded-2xl border-border bg-card text-foreground shadow-2xl backdrop-blur-xl"
|
|
|
|
|
|
>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
端点健康
|
2026-05-01 02:14:49 +08:00
|
|
|
|
<template #filter="{ close }">
|
|
|
|
|
|
<TableFilterMenu
|
|
|
|
|
|
v-model="filterApiFormat"
|
|
|
|
|
|
:options="apiFormatFilters"
|
|
|
|
|
|
@select="close"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</SortableTableHead>
|
|
|
|
|
|
<SortableTableHead
|
|
|
|
|
|
class="w-[8%] min-w-[60px] text-center"
|
|
|
|
|
|
column-key="status"
|
|
|
|
|
|
:sortable="false"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
:filter-active="filterStatus !== 'all'"
|
|
|
|
|
|
filter-title="筛选状态"
|
|
|
|
|
|
filter-content-class="w-40 p-1 rounded-2xl border-border bg-card text-foreground shadow-2xl backdrop-blur-xl"
|
|
|
|
|
|
>
|
2025-12-12 16:15:54 +08:00
|
|
|
|
状态
|
2026-05-01 02:14:49 +08:00
|
|
|
|
<template #filter="{ close }">
|
|
|
|
|
|
<TableFilterMenu
|
|
|
|
|
|
v-model="filterStatus"
|
|
|
|
|
|
:options="statusFilters"
|
|
|
|
|
|
@select="close"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</SortableTableHead>
|
2026-02-03 18:48:39 +08:00
|
|
|
|
<TableHead class="w-[18%] min-w-[120px] text-center">
|
2025-12-12 16:15:54 +08:00
|
|
|
|
操作
|
|
|
|
|
|
</TableHead>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
2026-02-14 20:06:38 +08:00
|
|
|
|
<ProviderTableRow
|
2026-03-15 23:13:11 +08:00
|
|
|
|
v-for="provider in displayedProviders"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:key="provider.id"
|
2026-02-14 20:06:38 +08:00
|
|
|
|
:provider="provider"
|
|
|
|
|
|
:editing-description-id="editingDescriptionId"
|
|
|
|
|
|
:is-balance-loading="isBalanceLoading"
|
|
|
|
|
|
:get-provider-balance="getProviderBalance"
|
|
|
|
|
|
:get-provider-balance-breakdown="getProviderBalanceBreakdown"
|
|
|
|
|
|
:get-provider-balance-error="getProviderBalanceError"
|
|
|
|
|
|
:get-provider-checkin="getProviderCheckin"
|
|
|
|
|
|
:get-provider-cookie-expired="getProviderCookieExpired"
|
|
|
|
|
|
:get-provider-balance-extra="getProviderBalanceExtra"
|
|
|
|
|
|
:format-balance-display="formatBalanceDisplay"
|
|
|
|
|
|
:format-reset-countdown="formatResetCountdown"
|
|
|
|
|
|
:get-quota-used-color-class="getQuotaUsedColorClass"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@mousedown="handleMouseDown"
|
2026-02-14 20:06:38 +08:00
|
|
|
|
@row-click="handleRowClick"
|
|
|
|
|
|
@view-detail="openProviderDrawer"
|
|
|
|
|
|
@edit-provider="openEditProviderDialog"
|
|
|
|
|
|
@open-ops-config="openOpsConfigDialog"
|
|
|
|
|
|
@toggle-status="toggleProviderStatus"
|
|
|
|
|
|
@delete-provider="handleDeleteProvider"
|
|
|
|
|
|
@start-edit-description="startEditDescription"
|
|
|
|
|
|
@save-description="saveDescription"
|
|
|
|
|
|
@cancel-edit-description="cancelEditDescription"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-13 22:26:47 +08:00
|
|
|
|
<!-- 移动端卡片列表 -->
|
|
|
|
|
|
<div
|
2026-03-09 12:45:54 +08:00
|
|
|
|
v-if="!loading && providers.length > 0"
|
2025-12-13 22:26:47 +08:00
|
|
|
|
class="xl:hidden divide-y divide-border/40"
|
|
|
|
|
|
>
|
2026-02-14 20:06:38 +08:00
|
|
|
|
<ProviderMobileCard
|
2026-03-15 23:13:11 +08:00
|
|
|
|
v-for="provider in displayedProviders"
|
2025-12-13 22:26:47 +08:00
|
|
|
|
:key="provider.id"
|
2026-02-14 20:06:38 +08:00
|
|
|
|
:provider="provider"
|
|
|
|
|
|
:editing-description-id="editingDescriptionId"
|
|
|
|
|
|
:is-balance-loading="isBalanceLoading"
|
|
|
|
|
|
:get-provider-balance="getProviderBalance"
|
|
|
|
|
|
:get-provider-balance-error="getProviderBalanceError"
|
|
|
|
|
|
:get-provider-checkin="getProviderCheckin"
|
|
|
|
|
|
:get-provider-cookie-expired="getProviderCookieExpired"
|
|
|
|
|
|
:format-balance-display="formatBalanceDisplay"
|
|
|
|
|
|
:get-quota-used-color-class="getQuotaUsedColorClass"
|
|
|
|
|
|
@view-detail="openProviderDrawer"
|
|
|
|
|
|
@edit-provider="openEditProviderDialog"
|
|
|
|
|
|
@open-ops-config="openOpsConfigDialog"
|
|
|
|
|
|
@toggle-status="toggleProviderStatus"
|
|
|
|
|
|
@delete-provider="handleDeleteProvider"
|
|
|
|
|
|
@start-edit-description="startEditDescription"
|
|
|
|
|
|
@save-description="saveDescription"
|
|
|
|
|
|
@cancel-edit-description="cancelEditDescription"
|
|
|
|
|
|
/>
|
2025-12-13 22:26:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
|
<Pagination
|
2026-03-09 12:45:54 +08:00
|
|
|
|
v-if="!loading && total > 0"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:current="currentPage"
|
2026-03-09 12:45:54 +08:00
|
|
|
|
:total="total"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:page-size="pageSize"
|
2026-01-17 20:21:03 +08:00
|
|
|
|
cache-key="provider-management-page-size"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@update:current="currentPage = $event"
|
|
|
|
|
|
@update:page-size="pageSize = $event"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 对话框 -->
|
|
|
|
|
|
<ProviderFormDialog
|
|
|
|
|
|
v-model="providerDialogOpen"
|
|
|
|
|
|
:provider="providerToEdit"
|
2026-02-06 16:37:06 +08:00
|
|
|
|
:max-priority="maxProviderPriority"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@provider-created="handleProviderAdded"
|
|
|
|
|
|
@provider-updated="handleProviderUpdated"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<PriorityManagementDialog
|
|
|
|
|
|
v-model="priorityDialogOpen"
|
|
|
|
|
|
@saved="handlePrioritySaved"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<ProviderDetailDrawer
|
|
|
|
|
|
:open="providerDrawerOpen"
|
|
|
|
|
|
:provider-id="selectedProviderId"
|
2026-03-31 19:19:04 +08:00
|
|
|
|
:initial-provider="selectedProvider"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@update:open="providerDrawerOpen = $event"
|
|
|
|
|
|
@edit="openEditProviderDialog"
|
|
|
|
|
|
@toggle-status="toggleProviderStatus"
|
2026-03-01 11:57:09 +08:00
|
|
|
|
@refresh="handleDrawerRefresh"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
/>
|
2026-01-17 19:50:35 +08:00
|
|
|
|
|
|
|
|
|
|
<ProviderAuthDialog
|
|
|
|
|
|
v-model:open="opsConfigDialogOpen"
|
|
|
|
|
|
:provider-id="opsConfigProviderId"
|
|
|
|
|
|
:provider-website="opsConfigProviderWebsite"
|
|
|
|
|
|
@saved="handleOpsConfigSaved"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-09 12:45:54 +08:00
|
|
|
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import Button from '@/components/ui/button.vue'
|
|
|
|
|
|
import Card from '@/components/ui/card.vue'
|
|
|
|
|
|
import Table from '@/components/ui/table.vue'
|
|
|
|
|
|
import TableHeader from '@/components/ui/table-header.vue'
|
|
|
|
|
|
import TableBody from '@/components/ui/table-body.vue'
|
|
|
|
|
|
import TableRow from '@/components/ui/table-row.vue'
|
|
|
|
|
|
import TableHead from '@/components/ui/table-head.vue'
|
2026-05-01 02:14:49 +08:00
|
|
|
|
import SortableTableHead from '@/components/ui/sortable-table-head.vue'
|
|
|
|
|
|
import TableFilterMenu from '@/components/ui/table-filter-menu.vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import Pagination from '@/components/ui/pagination.vue'
|
2026-01-17 19:50:35 +08:00
|
|
|
|
import { ProviderFormDialog, PriorityManagementDialog, ProviderAuthDialog } from '@/features/providers/components'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import ProviderDetailDrawer from '@/features/providers/components/ProviderDetailDrawer.vue'
|
2026-02-14 20:06:38 +08:00
|
|
|
|
import ProviderTableHeader from '@/features/providers/components/ProviderTableHeader.vue'
|
|
|
|
|
|
import ProviderTableRow from '@/features/providers/components/ProviderTableRow.vue'
|
|
|
|
|
|
import ProviderMobileCard from '@/features/providers/components/ProviderMobileCard.vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
|
|
import { useConfirm } from '@/composables/useConfirm'
|
|
|
|
|
|
import { useRowClick } from '@/composables/useRowClick'
|
2026-02-14 20:06:38 +08:00
|
|
|
|
import { useProviderFilters } from '@/features/providers/composables/useProviderFilters'
|
|
|
|
|
|
import { useProviderBalance } from '@/features/providers/composables/useProviderBalance'
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getProvidersSummary,
|
2026-03-01 11:57:09 +08:00
|
|
|
|
getProvider,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
deleteProvider,
|
2026-03-12 01:11:35 +08:00
|
|
|
|
getProviderDeleteTask,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
updateProvider,
|
2026-02-06 21:52:22 +08:00
|
|
|
|
getGlobalModels,
|
2026-01-21 01:10:35 +08:00
|
|
|
|
type ProviderWithEndpointsSummary,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} from '@/api/endpoints'
|
|
|
|
|
|
import { adminApi } from '@/api/admin'
|
2026-02-22 00:43:41 +08:00
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
2026-02-14 17:34:44 +08:00
|
|
|
|
|
2026-03-12 01:11:35 +08:00
|
|
|
|
interface ProviderDeleteProgressState {
|
|
|
|
|
|
providerId: string
|
|
|
|
|
|
providerName: string
|
|
|
|
|
|
taskId: string
|
|
|
|
|
|
status: string
|
|
|
|
|
|
stage: string
|
|
|
|
|
|
totalKeys: number
|
|
|
|
|
|
deletedKeys: number
|
|
|
|
|
|
totalEndpoints: number
|
|
|
|
|
|
deletedEndpoints: number
|
|
|
|
|
|
message: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { error: showError, success: showSuccess, info: showInfo } = useToast()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const { confirmDanger } = useConfirm()
|
|
|
|
|
|
|
|
|
|
|
|
// 状态
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
2026-03-03 22:04:40 +08:00
|
|
|
|
let providersRequestId = 0
|
2025-12-10 20:52:44 +08:00
|
|
|
|
const providerDialogOpen = ref(false)
|
|
|
|
|
|
const providerToEdit = ref<ProviderWithEndpointsSummary | null>(null)
|
|
|
|
|
|
const priorityDialogOpen = ref(false)
|
|
|
|
|
|
const priorityMode = ref<'provider' | 'global_key'>('provider')
|
|
|
|
|
|
const providerDrawerOpen = ref(false)
|
|
|
|
|
|
const selectedProviderId = ref<string | null>(null)
|
2026-03-31 19:19:04 +08:00
|
|
|
|
const selectedProvider = computed<ProviderWithEndpointsSummary | null>(() => {
|
|
|
|
|
|
if (!selectedProviderId.value) return null
|
|
|
|
|
|
return providers.value.find(provider => provider.id === selectedProviderId.value) ?? null
|
|
|
|
|
|
})
|
2026-03-12 01:11:35 +08:00
|
|
|
|
const providerDeleteProgress = ref<ProviderDeleteProgressState | null>(null)
|
|
|
|
|
|
let deletePollAbort: AbortController | null = null
|
|
|
|
|
|
|
|
|
|
|
|
const DELETE_POLL_INTERVAL_MS = 2000
|
|
|
|
|
|
const DELETE_POLL_MAX_MS = 30 * 60 * 1000
|
|
|
|
|
|
const DELETE_POLL_MAX_FAILURES = 3
|
2026-04-19 15:17:25 +08:00
|
|
|
|
const PROVIDER_SUMMARY_CACHE_TTL_MS = 10 * 1000
|
|
|
|
|
|
const PROVIDER_PRIORITY_MODE_CACHE_TTL_MS = 30 * 1000
|
|
|
|
|
|
const PROVIDER_MODEL_FILTER_CACHE_TTL_MS = 10 * 1000
|
2026-03-12 01:11:35 +08:00
|
|
|
|
|
|
|
|
|
|
async function pollProviderDeleteTask(providerId: string, taskId: string) {
|
|
|
|
|
|
deletePollAbort?.abort()
|
|
|
|
|
|
const abort = new AbortController()
|
|
|
|
|
|
deletePollAbort = abort
|
|
|
|
|
|
|
|
|
|
|
|
const deadline = Date.now() + DELETE_POLL_MAX_MS
|
|
|
|
|
|
let consecutiveFailures = 0
|
|
|
|
|
|
|
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
|
if (abort.signal.aborted) return null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const task = await getProviderDeleteTask(providerId, taskId)
|
|
|
|
|
|
consecutiveFailures = 0
|
|
|
|
|
|
if (providerDeleteProgress.value?.taskId === taskId) {
|
|
|
|
|
|
providerDeleteProgress.value = {
|
|
|
|
|
|
...providerDeleteProgress.value,
|
|
|
|
|
|
status: task.status,
|
|
|
|
|
|
stage: task.stage,
|
|
|
|
|
|
totalKeys: task.total_keys,
|
|
|
|
|
|
deletedKeys: task.deleted_keys,
|
|
|
|
|
|
totalEndpoints: task.total_endpoints,
|
|
|
|
|
|
deletedEndpoints: task.deleted_endpoints,
|
|
|
|
|
|
message: task.message,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (task.status === 'completed' || task.status === 'failed') {
|
|
|
|
|
|
return task
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
consecutiveFailures += 1
|
|
|
|
|
|
if (consecutiveFailures >= DELETE_POLL_MAX_FAILURES) {
|
|
|
|
|
|
throw new Error('provider delete task polling failed')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
|
const timer = setTimeout(resolve, DELETE_POLL_INTERVAL_MS)
|
|
|
|
|
|
abort.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(undefined) }, { once: true })
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new Error('provider delete task timeout')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const providerDeleteStageLabel = computed(() => {
|
|
|
|
|
|
switch (providerDeleteProgress.value?.stage) {
|
|
|
|
|
|
case 'preparing':
|
|
|
|
|
|
return '准备删除'
|
|
|
|
|
|
case 'disabling':
|
|
|
|
|
|
return '停用提供商'
|
|
|
|
|
|
case 'cleaning_restrictions':
|
|
|
|
|
|
return '清理访问限制'
|
|
|
|
|
|
case 'cleaning_provider_refs':
|
|
|
|
|
|
return '清理历史引用'
|
|
|
|
|
|
case 'deleting_keys':
|
|
|
|
|
|
return '删除号池账号'
|
|
|
|
|
|
case 'deleting_endpoints':
|
|
|
|
|
|
return '删除端点'
|
|
|
|
|
|
case 'completed':
|
|
|
|
|
|
return '删除完成'
|
|
|
|
|
|
case 'failed':
|
|
|
|
|
|
return '删除失败'
|
|
|
|
|
|
default:
|
|
|
|
|
|
return '等待执行'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerDeleteTotalUnits = computed(() => {
|
|
|
|
|
|
const progress = providerDeleteProgress.value
|
|
|
|
|
|
if (!progress) return 0
|
|
|
|
|
|
return progress.totalKeys + progress.totalEndpoints
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerDeleteCompletedUnits = computed(() => {
|
|
|
|
|
|
const progress = providerDeleteProgress.value
|
|
|
|
|
|
if (!progress) return 0
|
|
|
|
|
|
return Math.min(progress.deletedKeys + progress.deletedEndpoints, providerDeleteTotalUnits.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerDeleteOverallPercent = computed(() => {
|
|
|
|
|
|
const progress = providerDeleteProgress.value
|
|
|
|
|
|
if (!progress) return 0
|
|
|
|
|
|
if (progress.status === 'completed') return 100
|
|
|
|
|
|
if (providerDeleteTotalUnits.value <= 0) return 0
|
|
|
|
|
|
return Math.min(
|
|
|
|
|
|
100,
|
|
|
|
|
|
Math.round((providerDeleteCompletedUnits.value / providerDeleteTotalUnits.value) * 100),
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerDeleteKeysPercent = computed(() => {
|
|
|
|
|
|
const progress = providerDeleteProgress.value
|
|
|
|
|
|
if (!progress?.totalKeys) return 0
|
|
|
|
|
|
return Math.min(100, Math.round((progress.deletedKeys / progress.totalKeys) * 100))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const providerDeleteEndpointsPercent = computed(() => {
|
|
|
|
|
|
const progress = providerDeleteProgress.value
|
|
|
|
|
|
if (!progress?.totalEndpoints) return 0
|
|
|
|
|
|
return Math.min(100, Math.round((progress.deletedEndpoints / progress.totalEndpoints) * 100))
|
|
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
// 全局模型数据(用于模型筛选下拉)
|
|
|
|
|
|
const globalModels = ref<{ id: string; name: string }[]>([])
|
2026-02-13 16:41:30 +08:00
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
// Composables
|
|
|
|
|
|
const {
|
|
|
|
|
|
searchQuery,
|
|
|
|
|
|
filterStatus,
|
|
|
|
|
|
filterApiFormat,
|
|
|
|
|
|
filterModel,
|
|
|
|
|
|
statusFilters,
|
|
|
|
|
|
apiFormatFilters,
|
|
|
|
|
|
modelFilters,
|
|
|
|
|
|
hasActiveFilters,
|
|
|
|
|
|
currentPage,
|
|
|
|
|
|
pageSize,
|
2026-03-09 12:45:54 +08:00
|
|
|
|
total,
|
|
|
|
|
|
queryParams,
|
2026-02-14 20:06:38 +08:00
|
|
|
|
resetFilters,
|
|
|
|
|
|
} = useProviderFilters(
|
|
|
|
|
|
() => globalModels.value,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
loadArchitectureSchemas,
|
|
|
|
|
|
loadBalances,
|
|
|
|
|
|
getProviderBalance,
|
|
|
|
|
|
getProviderBalanceBreakdown,
|
|
|
|
|
|
getProviderBalanceError,
|
|
|
|
|
|
isBalanceLoading,
|
|
|
|
|
|
getProviderCheckin,
|
|
|
|
|
|
getProviderCookieExpired,
|
|
|
|
|
|
formatBalanceDisplay,
|
|
|
|
|
|
formatResetCountdown,
|
|
|
|
|
|
getProviderBalanceExtra,
|
|
|
|
|
|
getQuotaUsedColorClass,
|
|
|
|
|
|
startTick,
|
|
|
|
|
|
stopTick,
|
|
|
|
|
|
} = useProviderBalance()
|
2026-02-13 16:41:30 +08:00
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
// 扩展操作配置对话框
|
|
|
|
|
|
const opsConfigDialogOpen = ref(false)
|
|
|
|
|
|
const opsConfigProviderId = ref('')
|
|
|
|
|
|
const opsConfigProviderWebsite = ref('')
|
|
|
|
|
|
|
2026-02-14 17:34:44 +08:00
|
|
|
|
// 内联编辑备注
|
|
|
|
|
|
const editingDescriptionId = ref<string | null>(null)
|
|
|
|
|
|
|
2026-03-15 23:13:11 +08:00
|
|
|
|
function sortProvidersByActiveAndPriority(items: ProviderWithEndpointsSummary[]) {
|
|
|
|
|
|
return [...items].sort((a, b) => {
|
|
|
|
|
|
if (a.is_active !== b.is_active) {
|
|
|
|
|
|
return a.is_active ? -1 : 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if (a.provider_priority !== b.provider_priority) {
|
|
|
|
|
|
return a.provider_priority - b.provider_priority
|
|
|
|
|
|
}
|
|
|
|
|
|
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const displayedProviders = computed(() => sortProvidersByActiveAndPriority(providers.value))
|
|
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
function startEditDescription(_event: Event, provider: ProviderWithEndpointsSummary) {
|
2026-02-14 17:34:44 +08:00
|
|
|
|
editingDescriptionId.value = provider.id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
function cancelEditDescription(_event?: Event) {
|
2026-02-14 17:34:44 +08:00
|
|
|
|
editingDescriptionId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 20:06:38 +08:00
|
|
|
|
async function saveDescription(_event: Event, provider: ProviderWithEndpointsSummary, newValue: string) {
|
|
|
|
|
|
const trimmed = newValue.trim()
|
2026-02-14 17:34:44 +08:00
|
|
|
|
const oldValue = provider.description || ''
|
2026-02-14 20:06:38 +08:00
|
|
|
|
if (trimmed === oldValue) {
|
2026-02-14 17:34:44 +08:00
|
|
|
|
cancelEditDescription()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2026-02-14 20:06:38 +08:00
|
|
|
|
await updateProvider(provider.id, { description: trimmed || null })
|
|
|
|
|
|
provider.description = trimmed || undefined
|
2026-02-14 17:34:44 +08:00
|
|
|
|
// 同步更新 providers 数组
|
|
|
|
|
|
const target = providers.value.find(p => p.id === provider.id)
|
|
|
|
|
|
if (target) {
|
2026-02-14 20:06:38 +08:00
|
|
|
|
target.description = trimmed || undefined
|
2026-02-14 17:34:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
cancelEditDescription()
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
|
showError(parseApiError(err, '更新备注失败'), '错误')
|
2026-02-14 17:34:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 优先级模式配置
|
|
|
|
|
|
const priorityModeConfig = computed(() => {
|
|
|
|
|
|
return {
|
2026-02-14 20:06:38 +08:00
|
|
|
|
label: priorityMode.value === 'global_key' ? '全局 Key 优先' : '提供商优先',
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
// 当前已有提供商的最大优先级
|
|
|
|
|
|
const maxProviderPriority = computed(() => {
|
|
|
|
|
|
if (providers.value.length === 0) return undefined
|
2026-02-14 20:06:38 +08:00
|
|
|
|
const priorities = providers.value
|
|
|
|
|
|
.map(p => p.provider_priority)
|
|
|
|
|
|
.filter(v => typeof v === 'number' && Number.isFinite(v))
|
2026-02-06 16:37:06 +08:00
|
|
|
|
return priorities.length > 0 ? Math.max(...priorities) : undefined
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 加载优先级模式
|
2026-04-19 15:17:25 +08:00
|
|
|
|
async function loadPriorityMode(options: { cacheTtlMs?: number } = {}) {
|
2025-12-10 20:52:44 +08:00
|
|
|
|
try {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
const response = await adminApi.getSystemConfig('provider_priority_mode', {
|
|
|
|
|
|
cacheTtlMs: options.cacheTtlMs ?? 0,
|
|
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if (response.value) {
|
|
|
|
|
|
priorityMode.value = response.value as 'provider' | 'global_key'
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
priorityMode.value = 'provider'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 21:52:22 +08:00
|
|
|
|
// 加载全局模型列表(用于模型筛选下拉)
|
2026-04-19 15:17:25 +08:00
|
|
|
|
async function loadGlobalModelList(options: { cacheTtlMs?: number } = {}) {
|
2026-02-06 21:52:22 +08:00
|
|
|
|
try {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
const response = await getGlobalModels(
|
|
|
|
|
|
{ is_active: true, limit: 1000 },
|
|
|
|
|
|
{ cacheTtlMs: options.cacheTtlMs ?? 0 },
|
|
|
|
|
|
)
|
2026-02-06 21:52:22 +08:00
|
|
|
|
globalModels.value = response.models.map(m => ({ id: m.id, name: m.name }))
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
globalModels.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 12:45:54 +08:00
|
|
|
|
// 加载提供商列表(服务端分页)
|
2026-04-19 15:17:25 +08:00
|
|
|
|
async function loadProviders(options: { cacheTtlMs?: number } = {}) {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
const requestId = ++providersRequestId
|
2025-12-10 20:52:44 +08:00
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
const response = await getProvidersSummary(queryParams.value, {
|
|
|
|
|
|
cacheTtlMs: options.cacheTtlMs ?? 0,
|
|
|
|
|
|
})
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (requestId !== providersRequestId) return
|
2026-03-09 12:45:54 +08:00
|
|
|
|
providers.value = response.items
|
|
|
|
|
|
total.value = response.total
|
2026-01-17 19:50:35 +08:00
|
|
|
|
// 异步加载配置了 ops 的 provider 的余额数据
|
2026-02-14 20:06:38 +08:00
|
|
|
|
loadBalances(providers.value)
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (requestId !== providersRequestId) return
|
2026-02-22 00:43:41 +08:00
|
|
|
|
showError(parseApiError(err, '加载提供商列表失败'), '错误')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
} finally {
|
2026-03-03 22:04:40 +08:00
|
|
|
|
if (requestId === providersRequestId) {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 12:45:54 +08:00
|
|
|
|
// 分页/筛选/搜索变化时重新加载
|
|
|
|
|
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
watch(queryParams, (newParams, oldParams) => {
|
|
|
|
|
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
|
|
|
|
// 搜索输入 debounce 300ms,其他变化立即执行
|
|
|
|
|
|
const isSearchOnly = newParams.search !== oldParams?.search &&
|
|
|
|
|
|
newParams.page === oldParams?.page &&
|
|
|
|
|
|
newParams.page_size === oldParams?.page_size &&
|
|
|
|
|
|
newParams.status === oldParams?.status &&
|
|
|
|
|
|
newParams.api_format === oldParams?.api_format &&
|
|
|
|
|
|
newParams.model_id === oldParams?.model_id
|
|
|
|
|
|
if (isSearchOnly) {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
debounceTimer = setTimeout(() => {
|
|
|
|
|
|
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
|
|
|
|
|
}, 300)
|
2026-03-09 12:45:54 +08:00
|
|
|
|
} else {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
2026-03-09 12:45:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 使用复用的行点击逻辑
|
|
|
|
|
|
const { handleMouseDown, shouldTriggerRowClick } = useRowClick()
|
|
|
|
|
|
|
|
|
|
|
|
// 处理行点击 - 只在非选中文本时打开抽屉
|
|
|
|
|
|
function handleRowClick(event: MouseEvent, providerId: string) {
|
|
|
|
|
|
if (!shouldTriggerRowClick(event)) return
|
|
|
|
|
|
openProviderDrawer(providerId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开添加提供商对话框
|
|
|
|
|
|
function openAddProviderDialog() {
|
|
|
|
|
|
providerToEdit.value = null
|
|
|
|
|
|
providerDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开优先级管理对话框
|
|
|
|
|
|
function openPriorityDialog() {
|
|
|
|
|
|
priorityDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开提供商详情抽屉
|
|
|
|
|
|
function openProviderDrawer(providerId: string) {
|
|
|
|
|
|
selectedProviderId.value = providerId
|
|
|
|
|
|
providerDrawerOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 14:01:22 +08:00
|
|
|
|
function mergeUpdatedProvider(updated: ProviderWithEndpointsSummary) {
|
|
|
|
|
|
const index = providers.value.findIndex(p => p.id === updated.id)
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
providers.value[index] = updated
|
|
|
|
|
|
loadBalances([updated], false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshProviderSnapshot(
|
|
|
|
|
|
providerId: string,
|
|
|
|
|
|
fallbackErrorMessage = '刷新提供商数据失败',
|
|
|
|
|
|
): Promise<ProviderWithEndpointsSummary | null> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await getProvider(providerId)
|
|
|
|
|
|
mergeUpdatedProvider(updated)
|
|
|
|
|
|
return updated
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
showError(parseApiError(err, fallbackErrorMessage), '错误')
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 打开编辑提供商对话框
|
2026-04-13 14:01:22 +08:00
|
|
|
|
async function openEditProviderDialog(provider: ProviderWithEndpointsSummary) {
|
|
|
|
|
|
const latest = await refreshProviderSnapshot(provider.id, '刷新提供商状态失败')
|
|
|
|
|
|
providerToEdit.value = latest ?? provider
|
2025-12-10 20:52:44 +08:00
|
|
|
|
providerDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 19:50:35 +08:00
|
|
|
|
// 打开扩展操作配置对话框
|
2026-05-21 01:24:17 +08:00
|
|
|
|
function openOpsConfigDialog(provider: ProviderWithEndpointsSummary) {
|
2026-01-17 19:50:35 +08:00
|
|
|
|
opsConfigProviderId.value = provider.id
|
|
|
|
|
|
opsConfigProviderWebsite.value = provider.website || ''
|
|
|
|
|
|
opsConfigDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 扩展操作配置保存回调
|
|
|
|
|
|
function handleOpsConfigSaved() {
|
|
|
|
|
|
opsConfigDialogOpen.value = false
|
2026-04-19 15:17:25 +08:00
|
|
|
|
void loadProviders()
|
2026-01-17 19:50:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
// 处理提供商编辑完成
|
2026-03-01 11:57:09 +08:00
|
|
|
|
function handleProviderUpdated(updated: ProviderWithEndpointsSummary) {
|
2026-04-13 14:01:22 +08:00
|
|
|
|
mergeUpdatedProvider(updated)
|
2026-03-01 11:57:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理详情抽屉内的刷新:只刷新当前查看的那一条提供商
|
|
|
|
|
|
async function handleDrawerRefresh() {
|
|
|
|
|
|
if (!selectedProviderId.value) return
|
2026-04-13 14:01:22 +08:00
|
|
|
|
await refreshProviderSnapshot(selectedProviderId.value)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 优先级保存成功回调
|
|
|
|
|
|
async function handlePrioritySaved() {
|
|
|
|
|
|
await loadProviders()
|
|
|
|
|
|
await loadPriorityMode()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理提供商添加
|
|
|
|
|
|
function handleProviderAdded() {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
void loadProviders()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除提供商
|
|
|
|
|
|
async function handleDeleteProvider(provider: ProviderWithEndpointsSummary) {
|
|
|
|
|
|
const confirmed = await confirmDanger(
|
|
|
|
|
|
'删除提供商',
|
2026-02-14 20:06:38 +08:00
|
|
|
|
`确定要删除提供商 "${provider.name}" 吗?\n\n这将同时删除其所有端点、密钥和配置。此操作不可恢复!`,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmed) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-12 01:11:35 +08:00
|
|
|
|
const result = await deleteProvider(provider.id)
|
|
|
|
|
|
providerDeleteProgress.value = {
|
|
|
|
|
|
providerId: provider.id,
|
|
|
|
|
|
providerName: provider.name,
|
|
|
|
|
|
taskId: result.task_id,
|
|
|
|
|
|
status: result.status,
|
|
|
|
|
|
stage: 'queued',
|
|
|
|
|
|
totalKeys: provider.total_keys || 0,
|
|
|
|
|
|
deletedKeys: 0,
|
|
|
|
|
|
totalEndpoints: provider.total_endpoints || 0,
|
|
|
|
|
|
deletedEndpoints: 0,
|
|
|
|
|
|
message: result.message || '删除任务已提交,后台处理中',
|
|
|
|
|
|
}
|
|
|
|
|
|
showInfo(result.message || '删除任务已提交,后台处理中')
|
|
|
|
|
|
|
|
|
|
|
|
const task = await pollProviderDeleteTask(provider.id, result.task_id)
|
|
|
|
|
|
if (!task) return // aborted
|
|
|
|
|
|
if (task.status === 'failed') {
|
|
|
|
|
|
throw new Error(task.message || 'provider delete task failed')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
showSuccess('提供商已删除')
|
2026-03-12 01:11:35 +08:00
|
|
|
|
providerDeleteProgress.value = null
|
2026-04-19 15:17:25 +08:00
|
|
|
|
void loadProviders()
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
2026-03-12 01:11:35 +08:00
|
|
|
|
providerDeleteProgress.value = null
|
2026-02-22 00:43:41 +08:00
|
|
|
|
showError(parseApiError(err, '删除提供商失败'), '错误')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 切换提供商状态
|
|
|
|
|
|
async function toggleProviderStatus(provider: ProviderWithEndpointsSummary) {
|
|
|
|
|
|
try {
|
2025-12-25 00:02:56 +08:00
|
|
|
|
const newStatus = !provider.is_active
|
|
|
|
|
|
await updateProvider(provider.id, { is_active: newStatus })
|
|
|
|
|
|
|
|
|
|
|
|
// 更新抽屉内部的 provider 对象
|
|
|
|
|
|
provider.is_active = newStatus
|
|
|
|
|
|
|
|
|
|
|
|
// 同时更新主页面 providers 数组中的对象,实现无感更新
|
|
|
|
|
|
const targetProvider = providers.value.find(p => p.id === provider.id)
|
|
|
|
|
|
if (targetProvider) {
|
|
|
|
|
|
targetProvider.is_active = newStatus
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showSuccess(newStatus ? '提供商已启用' : '提供商已停用')
|
2026-02-22 00:43:41 +08:00
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
|
showError(parseApiError(err, '操作失败'), '错误')
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-14 17:34:44 +08:00
|
|
|
|
// 点击外部自动取消编辑备注
|
|
|
|
|
|
function handleGlobalClick(event: MouseEvent) {
|
|
|
|
|
|
if (!editingDescriptionId.value) return
|
|
|
|
|
|
const target = event.target as HTMLElement
|
|
|
|
|
|
if (target.closest('[data-desc-editor]')) return
|
|
|
|
|
|
cancelEditDescription()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
onMounted(() => {
|
2026-04-19 15:17:25 +08:00
|
|
|
|
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
|
|
|
|
|
void loadPriorityMode({ cacheTtlMs: PROVIDER_PRIORITY_MODE_CACHE_TTL_MS })
|
|
|
|
|
|
void loadGlobalModelList({ cacheTtlMs: PROVIDER_MODEL_FILTER_CACHE_TTL_MS })
|
|
|
|
|
|
void loadArchitectureSchemas()
|
2026-02-14 17:34:44 +08:00
|
|
|
|
document.addEventListener('click', handleGlobalClick, true)
|
2026-01-19 17:18:03 +08:00
|
|
|
|
// 每秒更新一次倒计时
|
2026-02-14 20:06:38 +08:00
|
|
|
|
startTick()
|
2026-01-19 17:18:03 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-03-12 01:11:35 +08:00
|
|
|
|
deletePollAbort?.abort()
|
2026-03-09 12:45:54 +08:00
|
|
|
|
if (debounceTimer) clearTimeout(debounceTimer)
|
2026-02-14 17:34:44 +08:00
|
|
|
|
document.removeEventListener('click', handleGlobalClick, true)
|
2026-02-14 20:06:38 +08:00
|
|
|
|
stopTick()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|