feat: 新增AI账号聚合视图(按平台分组+余额合计+预警+一键加号)

This commit is contained in:
gouki
2026-08-01 23:59:30 +00:00
parent e672086b67
commit f79d021307
+79 -5
View File
@@ -83,6 +83,17 @@ function openAssetCreate(presetType) {
if (presetType) form.asset_type = presetType; if (presetType) form.asset_type = presetType;
store.assetModal = { show: true, editing: null, form }; store.assetModal = { show: true, editing: null, form };
} }
function openAICreate(provider) {
const form = emptyAssetForm();
form.asset_type = 'ai_agent';
if (provider) {
form.provider = provider;
form.ai_detail.provider = provider;
const p = store.providers.find(x => x.slug === provider);
if (p) form.provider_id = p.id;
}
store.assetModal = { show: true, editing: null, form };
}
function openAssetEdit(a) { function openAssetEdit(a) {
const form = emptyAssetForm(); const form = emptyAssetForm();
['name', 'asset_type', 'provider', 'provider_id', 'renewal_cycle', 'account', 'expiry_date', 'auto_renew', 'cost', 'currency', 'status', 'is_archived', 'remark'].forEach(k => { form[k] = a[k]; }); ['name', 'asset_type', 'provider', 'provider_id', 'renewal_cycle', 'account', 'expiry_date', 'auto_renew', 'cost', 'currency', 'status', 'is_archived', 'remark'].forEach(k => { form[k] = a[k]; });
@@ -406,6 +417,66 @@ const DomainsView = {
}, },
}; };
const AIAccountsView = {
template: `
<div class="space-y-4">
<div class="flex justify-between items-center flex-wrap gap-2">
<div class="text-sm text-slate-500">共 {{ totalAccounts }} 个账号 · 总余额 {{ totalBalance }}</div>
<button @click="addAny" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700">+ 新增账号</button>
</div>
<div v-for="g in groups" :key="g.provider" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="flex justify-between items-center mb-3 flex-wrap gap-2">
<div class="flex items-center gap-2">
<span class="font-semibold text-sm">{{ g.provider }}</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-violet-500/10 text-violet-600 dark:text-violet-400">{{ g.accounts.length }} 个账号</span>
</div>
<div class="flex items-center gap-3 text-xs">
<span class="text-slate-500">余额合计 <b class="text-slate-800 dark:text-slate-200">{{ g.totalBalance }}</b></span>
<button @click="add(g.provider)" class="text-blue-600 dark:text-blue-400 hover:underline">+ 添加账号</button>
</div>
</div>
<div class="space-y-2">
<div v-for="a in g.accounts" :key="a.id" class="flex items-center justify-between px-3 py-2 rounded-lg bg-slate-50 dark:bg-slate-800/50 text-sm gap-2">
<div class="min-w-0">
<span class="font-medium">{{ a.name }}</span>
<span v-if="a.ai_detail && a.ai_detail.plan" class="text-xs text-slate-400 ml-2">{{ a.ai_detail.plan }}</span>
</div>
<div class="flex items-center gap-3 text-xs shrink-0">
<span :class="balanceClass(a.ai_detail && a.ai_detail.balance)">余额 {{ fmtBal(a.ai_detail && a.ai_detail.balance) }}{{ a.ai_detail && a.ai_detail.balance !== null && a.ai_detail.balance !== undefined ? ' ' + a.ai_detail.currency : '' }}</span>
<span v-if="a.ai_detail && a.ai_detail.monthly_usage !== null && a.ai_detail.monthly_usage !== undefined" class="text-slate-500">用量 {{ a.ai_detail.monthly_usage }}/{{ a.ai_detail.monthly_limit ?? '∞' }}</span>
<button @click="edit(a)" class="text-blue-600 dark:text-blue-400 hover:underline">编辑</button>
</div>
</div>
</div>
</div>
<div v-if="!groups.length" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-8 text-center text-slate-400 text-sm">暂无 AI 账号,点右上角「新增账号」添加(同一平台可添加多个)</div>
</div>`,
setup() {
const aiList = () => store.assets.filter(a => a.asset_type === 'ai_agent');
const groups = computed(() => {
const map = {};
for (const a of aiList()) {
const prov = (a.ai_detail && a.ai_detail.provider) || a.provider || '其他';
if (!map[prov]) map[prov] = { provider: prov, accounts: [], totalBalance: 0 };
map[prov].accounts.push(a);
const bal = a.ai_detail && a.ai_detail.balance;
if (bal) map[prov].totalBalance += Number(bal);
}
return Object.values(map).map(g => ({ ...g, totalBalance: Math.round(g.totalBalance * 100) / 100 })).sort((x, y) => y.totalBalance - x.totalBalance);
});
const totalAccounts = computed(() => aiList().length);
const totalBalance = computed(() => Math.round(aiList().reduce((s, a) => s + (a.ai_detail && a.ai_detail.balance ? Number(a.ai_detail.balance) : 0), 0) * 100) / 100);
function balanceClass(b) {
if (b === null || b === undefined) return 'text-slate-400';
if (Number(b) < 10) return 'text-red-600 dark:text-red-400 font-semibold';
if (Number(b) < 50) return 'text-amber-600 dark:text-amber-400';
return 'text-emerald-600 dark:text-emerald-400';
}
function fmtBal(b) { return (b === null || b === undefined) ? '—' : b; }
return { store, groups, totalAccounts, totalBalance, balanceClass, fmtBal, add: openAICreate, addAny: () => openAICreate(''), edit: openAssetEdit };
},
};
const SettingsView = { const SettingsView = {
template: ` template: `
<div class="max-w-lg space-y-4"> <div class="max-w-lg space-y-4">
@@ -621,17 +692,19 @@ const AppRoot = {
{ key: 'assets', label: '资产', icon: '📦' }, { key: 'assets', label: '资产', icon: '📦' },
{ key: 'servers', label: '服务器', icon: '🖥️' }, { key: 'servers', label: '服务器', icon: '🖥️' },
{ key: 'domains', label: '域名', icon: '🌐' }, { key: 'domains', label: '域名', icon: '🌐' },
{ key: 'ai', label: 'AI账号', icon: '🤖' },
{ key: 'providers', label: '平台', icon: '🏢' }, { key: 'providers', label: '平台', icon: '🏢' },
{ key: 'settings', label: '设置', icon: '⚙️' }, { key: 'settings', label: '设置', icon: '⚙️' },
]; ];
const viewMap = { dashboard: 'dashboard-view', assets: 'assets-view', servers: 'servers-view', domains: 'domains-view', providers: 'providers-view', settings: 'settings-view' }; const viewMap = { dashboard: 'dashboard-view', assets: 'assets-view', servers: 'servers-view', domains: 'domains-view', ai: 'ai-view', providers: 'providers-view', settings: 'settings-view' };
const currentNav = computed(() => navs.find(n => n.key === store.view) || navs[0]); const currentNav = computed(() => navs.find(n => n.key === store.view) || navs[0]);
const viewComponent = computed(() => viewMap[store.view] || 'dashboard-view'); const viewComponent = computed(() => viewMap[store.view] || 'dashboard-view');
function go(key) { store.view = key; if (key !== 'servers') store.serverMetrics = {}; } function go(key) { store.view = key; if (key !== 'servers') store.serverMetrics = {}; if (key === 'ai') { store.filterType = ''; store.filterStatus = ''; store.search = ''; loadAssets(); } }
function quickAdd() { function quickAdd() {
const preset = store.view === 'domains' ? 'domain' : (store.view === 'providers' ? null : 'vps'); if (store.view === 'providers') return openProviderCreate();
if (store.view === 'providers') openProviderCreate(); if (store.view === 'ai') return openAICreate('');
else openAssetCreate(preset); const preset = store.view === 'domains' ? 'domain' : 'vps';
openAssetCreate(preset);
} }
function toggleDark() { store.dark = !store.dark; applyDark(); } function toggleDark() { store.dark = !store.dark; applyDark(); }
return { store, appName, navs, currentNav, viewComponent, go, quickAdd, toggleDark, reload: loadAssets }; return { store, appName, navs, currentNav, viewComponent, go, quickAdd, toggleDark, reload: loadAssets };
@@ -645,6 +718,7 @@ app.component('assets-view', AssetsView);
app.component('providers-view', ProvidersView); app.component('providers-view', ProvidersView);
app.component('servers-view', ServersView); app.component('servers-view', ServersView);
app.component('domains-view', DomainsView); app.component('domains-view', DomainsView);
app.component('ai-view', AIAccountsView);
app.component('settings-view', SettingsView); app.component('settings-view', SettingsView);
applyDark(); applyDark();
loadAll(); loadAll();