feat: 平台卡片「关联N个资产」点击跳转资产页并按平台筛选(后端/api/assets新增provider参数,资产页可清除筛选标签)
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,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 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user