feat: 平台卡片「关联N个资产」点击跳转资产页并按平台筛选(后端/api/assets新增provider参数,资产页可清除筛选标签)

This commit is contained in:
gouki
2026-08-09 00:13:16 +00:00
parent 74f9688ba1
commit ee5a4c81cf
6 changed files with 43 additions and 6 deletions
+1 -1
View File
@@ -1 +1 @@
0.3.4 0.3.5
+3 -2
View File
@@ -20,15 +20,16 @@ router = APIRouter(prefix="/api/assets", tags=["assets"])
@router.get("", response_model=List[AssetRead], summary="资产列表") @router.get("", response_model=List[AssetRead], summary="资产列表")
def list_assets( def list_assets(
asset_type: Optional[AssetType] = Query(default=None, description="按类型筛选"), asset_type: Optional[AssetType] = Query(default=None, description="按类型筛选"),
status: Optional[AssetStatus] = Query(default=None, description="按状态筛选"), status_filter: Optional[AssetStatus] = Query(default=None, alias="status", description="按状态筛选"),
is_archived: Optional[bool] = Query(default=None, description="按归档标记筛选"), is_archived: Optional[bool] = Query(default=None, description="按归档标记筛选"),
q: Optional[str] = Query(default=None, description="搜索名称/服务商"), q: Optional[str] = Query(default=None, description="搜索名称/服务商"),
provider: Optional[str] = Query(default=None, description="按平台筛选(slug 或平台 ID"),
sort: str = Query(default="expiry_date", description="排序字段"), sort: str = Query(default="expiry_date", description="排序字段"),
order: str = Query(default="asc", description="排序方向 asc/desc"), order: str = Query(default="asc", description="排序方向 asc/desc"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
): ):
return asset_service.list_assets( return asset_service.list_assets(
session, asset_type, status, is_archived, q, sort, order session, asset_type, status_filter, is_archived, q, provider, sort, order
) )
+8
View File
@@ -7,6 +7,7 @@ from datetime import date, datetime, timedelta, timezone
from typing import List, Optional from typing import List, Optional
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy import or_
from sqlmodel import Session, select from sqlmodel import Session, select
from app.core import crypto from app.core import crypto
@@ -358,6 +359,7 @@ def list_assets(
asset_status: Optional[AssetStatus] = None, asset_status: Optional[AssetStatus] = None,
is_archived: Optional[bool] = None, is_archived: Optional[bool] = None,
q: Optional[str] = None, q: Optional[str] = None,
provider: Optional[str] = None,
sort: str = "expiry_date", sort: str = "expiry_date",
order: str = "asc", order: str = "asc",
) -> List[AssetRead]: ) -> List[AssetRead]:
@@ -369,6 +371,12 @@ def list_assets(
stmt = stmt.where(Asset.status == asset_status) stmt = stmt.where(Asset.status == asset_status)
if is_archived is not None: if is_archived is not None:
stmt = stmt.where(Asset.is_archived == is_archived) stmt = stmt.where(Asset.is_archived == is_archived)
if provider:
# 兼容两种关联方式:provider slug 文本或 provider_id 外键
conds = [Asset.provider == provider]
if provider.isdigit():
conds.append(Asset.provider_id == int(provider))
stmt = stmt.where(or_(*conds))
if q: if q:
pattern = f"%{q}%" pattern = f"%{q}%"
stmt = stmt.where(Asset.name.like(pattern) | Asset.provider.like(pattern)) stmt = stmt.where(Asset.name.like(pattern) | Asset.provider.like(pattern))
+15
View File
@@ -17,6 +17,7 @@ const store = reactive({
search: '', search: '',
filterType: '', filterType: '',
filterStatus: '', filterStatus: '',
filterProvider: '', // 按平台筛选(slug),从平台页「关联 N 个资产」跳转而来
assetTab: '', // 资产页 TAB'' 全部 / vps / domain / cloudflare / subscription(付费资产) / ai_agent assetTab: '', // 资产页 TAB'' 全部 / vps / domain / cloudflare / subscription(付费资产) / ai_agent
monitorTab: '', // 监控中心 TAB:同上 monitorTab: '', // 监控中心 TAB:同上
dark: localStorage.getItem('vps_dark') === '1', dark: localStorage.getItem('vps_dark') === '1',
@@ -76,6 +77,7 @@ async function loadAssets() {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (store.filterType) params.set('asset_type', store.filterType); if (store.filterType) params.set('asset_type', store.filterType);
if (store.filterStatus) params.set('status', store.filterStatus); if (store.filterStatus) params.set('status', store.filterStatus);
if (store.filterProvider) params.set('provider', store.filterProvider);
if (store.search) params.set('q', store.search); if (store.search) params.set('q', store.search);
params.set('sort', 'expiry_date'); params.set('order', 'asc'); params.set('sort', 'expiry_date'); params.set('order', 'asc');
store.assets = await Api.get('/assets?' + params.toString()); store.assets = await Api.get('/assets?' + params.toString());
@@ -197,6 +199,19 @@ async function deleteAccount(a) {
catch (e) { store.error = e.message; } catch (e) { store.error = e.message; }
} }
/* ---------------- 平台页跳转:按平台查看资产 ---------------- */
function openAssetsByProvider(p) {
store.filterProvider = p.slug;
store.assetTab = '';
store.filterType = '';
if (window.location.hash !== '#assets') window.location.hash = '#assets';
loadAssets();
}
function clearProviderFilter() {
store.filterProvider = '';
loadAssets();
}
/* ---------------- 服务器监控 ---------------- */ /* ---------------- 服务器监控 ---------------- */
async function viewServer(a) { async function viewServer(a) {
try { try {
+14 -1
View File
@@ -14,6 +14,14 @@ const AssetsView = {
</button> </button>
</div> </div>
<!-- 平台筛选标签(从平台页跳转而来,点 ✕ 取消) -->
<div v-if="store.filterProvider" class="flex items-center">
<span class="inline-flex items-center gap-1 text-xs px-2.5 py-1 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400">
🏢 平台:{{ providerName(store.filterProvider) }}
<button @click="clearProvider" class="font-bold hover:opacity-70" title="取消平台筛选">✕</button>
</span>
</div>
<!-- 通用列表(全部 / VPS --> <!-- 通用列表(全部 / VPS -->
<template v-if="store.assetTab==='' || store.assetTab==='vps'"> <template v-if="store.assetTab==='' || store.assetTab==='vps'">
<div class="flex flex-wrap gap-2 items-center"> <div class="flex flex-wrap gap-2 items-center">
@@ -116,6 +124,11 @@ const AssetsView = {
// 专有视图需要全量数据;若此前按类型过滤过则清空重载 // 专有视图需要全量数据;若此前按类型过滤过则清空重载
if (k !== '' && k !== 'vps' && store.filterType) { store.filterType = ''; loadAssets(); } if (k !== '' && k !== 'vps' && store.filterType) { store.filterType = ''; loadAssets(); }
} }
// 平台筛选标签显示名:filterProvider 存 slug,回退显示原值
function providerName(slug) {
const p = store.providers.find(x => x.slug === slug);
return p ? p.name : slug;
}
async function refreshBalance(a) { async function refreshBalance(a) {
if (!confirm('刷新「' + a.name + '」的余额?')) return; if (!confirm('刷新「' + a.name + '」的余额?')) return;
try { try {
@@ -132,6 +145,6 @@ const AssetsView = {
await loadAssets(); await loadAssets();
} catch (e) { alert('同步失败:' + e.message); } } catch (e) { alert('同步失败:' + e.message); }
} }
return { store, Fmt, tabs, tabView, list, switchTab, reload: loadAssets, edit: openAssetEdit, del: deleteAsset, viewServer, refreshBalance, syncAllAI }; return { store, Fmt, tabs, tabView, list, switchTab, providerName, reload: loadAssets, clearProvider: clearProviderFilter, edit: openAssetEdit, del: deleteAsset, viewServer, refreshBalance, syncAllAI };
}, },
}; };
+2 -2
View File
@@ -28,7 +28,7 @@ const ProvidersView = {
<div v-if="p.sdk_type">API 对接: {{ p.sdk_type }}</div> <div v-if="p.sdk_type">API 对接: {{ p.sdk_type }}</div>
<div v-if="p.console_url"><a :href="p.console_url" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">管理面板 ↗</a></div> <div v-if="p.console_url"><a :href="p.console_url" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">管理面板 ↗</a></div>
<div class="flex items-center gap-2 mt-1"> <div class="flex items-center gap-2 mt-1">
<span class="text-slate-500">📎 关联 {{ countOf(p) }} 个资产</span> <button @click="goAssets(p)" :disabled="!countOf(p)" class="text-slate-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:hover:text-slate-500" :title="countOf(p) ? '查看该平台下的资产' : ''">📎 关联 {{ countOf(p) }} 个资产</button>
<button @click="openAccounts(p)" class="text-slate-500 hover:text-blue-600 dark:hover:text-blue-400">👤 账号 {{ accountsCountOf(p) }}</button> <button @click="openAccounts(p)" class="text-slate-500 hover:text-blue-600 dark:hover:text-blue-400">👤 账号 {{ accountsCountOf(p) }}</button>
<span v-if="p.has_api_config" class="text-emerald-600 dark:text-emerald-400">已配置 API</span> <span v-if="p.has_api_config" class="text-emerald-600 dark:text-emerald-400">已配置 API</span>
<span v-else class="text-slate-400">未配置 API</span> <span v-else class="text-slate-400">未配置 API</span>
@@ -118,6 +118,6 @@ const ProvidersView = {
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; } } catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
finally { syncing[p.id] = false; } finally { syncing[p.id] = false; }
} }
return { store, Fmt, testing, syncing, results, countOf, accountsCountOf, serviceList, showAll, toggleShowAll, visibleProviders, hiddenCount, isSupported, canSync, fmtTime, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider, openAccounts: (p) => openAccountsView(p.slug), manageAccounts: () => openAccountsView(null) }; return { store, Fmt, testing, syncing, results, countOf, accountsCountOf, serviceList, showAll, toggleShowAll, visibleProviders, hiddenCount, isSupported, canSync, fmtTime, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider, openAccounts: (p) => openAccountsView(p.slug), manageAccounts: () => openAccountsView(null), goAssets: openAssetsByProvider };
}, },
}; };