feat(providers): provider 摘要列表排序增加启用状态优先

后端查询和前端展示均按 is_active 降序、priority 升序、created_at 升序排列,
确保已启用的 provider 始终排在前面。
This commit is contained in:
fawney19
2026-03-15 23:13:11 +08:00
parent faba0cbd07
commit 7ff63077c3
3 changed files with 111 additions and 4 deletions

View File

@@ -154,7 +154,7 @@
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<ProviderTableRow <ProviderTableRow
v-for="provider in providers" v-for="provider in displayedProviders"
:key="provider.id" :key="provider.id"
:provider="provider" :provider="provider"
:editing-description-id="editingDescriptionId" :editing-description-id="editingDescriptionId"
@@ -189,7 +189,7 @@
class="xl:hidden divide-y divide-border/40" class="xl:hidden divide-y divide-border/40"
> >
<ProviderMobileCard <ProviderMobileCard
v-for="provider in providers" v-for="provider in displayedProviders"
:key="provider.id" :key="provider.id"
:provider="provider" :provider="provider"
:editing-description-id="editingDescriptionId" :editing-description-id="editingDescriptionId"
@@ -468,6 +468,20 @@ const opsConfigProviderWebsite = ref('')
// 内联编辑备注 // 内联编辑备注
const editingDescriptionId = ref<string | null>(null) const editingDescriptionId = ref<string | null>(null)
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))
function startEditDescription(_event: Event, provider: ProviderWithEndpointsSummary) { function startEditDescription(_event: Event, provider: ProviderWithEndpointsSummary) {
editingDescriptionId.value = provider.id editingDescriptionId.value = provider.id
} }

View File

@@ -4,7 +4,7 @@ Provider 摘要与健康监控 API
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any from typing import Any, Protocol
from fastapi import APIRouter, Depends, Query, Request from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import case, func from sqlalchemy import case, func
@@ -43,10 +43,26 @@ from src.services.cache.model_cache import ModelCacheService
from src.services.cache.provider_cache import ProviderCacheService from src.services.cache.provider_cache import ProviderCacheService
from src.utils.cache_decorator import cache_result from src.utils.cache_decorator import cache_result
class _HasProviderSortFields(Protocol):
is_active: Any
provider_priority: Any
created_at: Any
router = APIRouter(tags=["Provider Summary"]) router = APIRouter(tags=["Provider Summary"])
pipeline = get_pipeline() pipeline = get_pipeline()
def _provider_summary_ordering(provider_model: _HasProviderSortFields) -> tuple[Any, Any, Any]:
"""Provider 摘要列表排序:启用在前,其次按优先级与创建时间。"""
return (
case((provider_model.is_active == True, 0), else_=1).asc(),
provider_model.provider_priority.asc(),
provider_model.created_at.asc(),
)
@router.get("/summary", response_model=ProviderSummaryPageResponse) @router.get("/summary", response_model=ProviderSummaryPageResponse)
async def get_providers_summary( async def get_providers_summary(
request: Request, request: Request,
@@ -821,7 +837,7 @@ class AdminProviderSummaryAdapter(AdminApiAdapter):
total = query.count() total = query.count()
providers = ( providers = (
query.order_by(Provider.provider_priority.asc(), Provider.created_at.asc()) query.order_by(*_provider_summary_ordering(Provider))
.offset((self.page - 1) * self.page_size) .offset((self.page - 1) * self.page_size)
.limit(self.page_size) .limit(self.page_size)
.all() .all()

View File

@@ -0,0 +1,77 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from sqlalchemy import Boolean, Column, DateTime, Integer, String, create_engine, select
from sqlalchemy.orm import Session, declarative_base
from src.api.admin.providers.summary import _provider_summary_ordering
Base = declarative_base()
class DemoProvider(Base):
__tablename__ = "demo_providers"
id = Column(String(36), primary_key=True)
name = Column(String(100), nullable=False)
is_active = Column(Boolean, nullable=False)
provider_priority = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), nullable=False)
def test_provider_summary_ordering_puts_inactive_providers_last() -> None:
engine = create_engine("sqlite:///:memory:")
try:
Base.metadata.create_all(engine)
with Session(engine) as db:
base_time = datetime(2026, 3, 15, tzinfo=timezone.utc)
db.add_all(
[
DemoProvider(
id="inactive-top-priority",
name="inactive-top-priority",
is_active=False,
provider_priority=0,
created_at=base_time,
),
DemoProvider(
id="active-lower-priority",
name="active-lower-priority",
is_active=True,
provider_priority=20,
created_at=base_time + timedelta(seconds=1),
),
DemoProvider(
id="active-higher-priority-newer",
name="active-higher-priority-newer",
is_active=True,
provider_priority=5,
created_at=base_time + timedelta(seconds=3),
),
DemoProvider(
id="active-higher-priority-older",
name="active-higher-priority-older",
is_active=True,
provider_priority=5,
created_at=base_time + timedelta(seconds=2),
),
]
)
db.commit()
ordered = (
db.execute(select(DemoProvider).order_by(*_provider_summary_ordering(DemoProvider)))
.scalars()
.all()
)
assert [provider.id for provider in ordered] == [
"active-higher-priority-older",
"active-higher-priority-newer",
"active-lower-priority",
"inactive-top-priority",
]
finally:
engine.dispose()