Files
vps-manager/static/js/views/assets.js
T

138 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 资产视图:TAB 容器(全部/VPS/域名/Cloudflare/订阅/AI账号)
* 注意:专有视图(DomainsView 等)在后续脚本才定义,必须在 setup 运行时求值,
* 不能在组件定义处直接引用(会因加载顺序抛 ReferenceError)。
*/
const AssetsView = {
template: `
<div class="space-y-3">
<!-- 类型 TAB -->
<div class="flex flex-wrap gap-1.5">
<button v-for="t in tabs" :key="t.key" @click="switchTab(t.key)" :title="t.tip"
class="text-sm px-3 py-1.5 rounded-full transition"
:class="store.assetTab===t.key ? 'bg-blue-600 text-white' : 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'">
{{ t.label }}
</button>
</div>
<!-- 通用列表(全部 / VPS -->
<template v-if="store.assetTab==='' || store.assetTab==='vps'">
<div class="flex flex-wrap gap-2 items-center">
<select v-model="store.filterStatus" @change="reload" class="text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900">
<option value="">全部状态</option>
<option v-for="(l,k) in Fmt.STATUS_LABELS" :key="k" :value="k">{{ l }}</option>
</select>
<button v-if="store.assetTab===''" @click="syncAllAI" class="text-sm px-3 py-1.5 rounded-lg border border-violet-300 dark:border-violet-700 text-violet-600 dark:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/20">同步AI余额</button>
<span class="ml-auto text-xs text-slate-400">共 {{ list.length }} 条</span>
</div>
<!-- 桌面表格 -->
<div class="hidden md:block bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 dark:bg-slate-800/50 text-slate-500 text-left text-xs">
<tr>
<th class="px-4 py-2.5 font-medium">名称</th>
<th class="px-4 py-2.5 font-medium">类型</th>
<th class="px-4 py-2.5 font-medium">平台</th>
<th class="px-4 py-2.5 font-medium">到期</th>
<th class="px-4 py-2.5 font-medium">费用</th>
<th class="px-4 py-2.5 font-medium">状态</th>
<th class="px-4 py-2.5 font-medium text-right">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
<tr v-if="!list.length"><td colspan="7" class="px-4 py-10 text-center text-slate-400">暂无资产</td></tr>
<tr v-for="a in list" :key="a.id" class="hover:bg-slate-50 dark:hover:bg-slate-800/30" :class="{ 'opacity-50': a.is_archived }">
<td class="px-4 py-2.5 font-medium">{{ a.name }}</td>
<td class="px-4 py-2.5"><span class="text-xs px-2 py-0.5 rounded-full" :class="Fmt.typeBadge(a.asset_type)">{{ Fmt.TYPE_LABELS[a.asset_type] }}</span></td>
<td class="px-4 py-2.5 text-slate-500">{{ a.provider_name || a.provider }}</td>
<td class="px-4 py-2.5" :class="Fmt.expiryText(a.days_to_expiry)">{{ a.expiry_date || '—' }}<span v-if="a.days_to_expiry!==null && a.days_to_expiry!==undefined" class="text-xs ml-1">({{ a.days_to_expiry }}天)</span></td>
<td class="px-4 py-2.5 text-slate-600 dark:text-slate-300">{{ a.cost }} {{ a.currency }}<span v-if="a.renewal_cycle" class="text-xs text-slate-400 ml-1">/{{ Fmt.CYCLE_LABELS[a.renewal_cycle] || a.renewal_cycle }}</span></td>
<td class="px-4 py-2.5"><span class="text-xs px-2 py-0.5 rounded-full" :class="Fmt.statusBadge(a.status)">{{ Fmt.STATUS_LABELS[a.status] }}</span></td>
<td class="px-4 py-2.5 text-right whitespace-nowrap">
<button v-if="a.asset_type==='vps'" @click="viewServer(a)" class="text-emerald-600 dark:text-emerald-400 hover:underline mr-3 text-xs">监控</button>
<button v-if="a.asset_type==='ai_agent'" @click="refreshBalance(a)" class="text-violet-600 dark:text-violet-400 hover:underline mr-3 text-xs">刷新余额</button>
<button @click="edit(a)" class="text-blue-600 dark:text-blue-400 hover:underline mr-3 text-xs">编辑</button>
<button @click="del(a)" class="text-red-600 dark:text-red-400 hover:underline text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 移动卡片 -->
<div class="md:hidden space-y-2">
<div v-if="!list.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">暂无资产</div>
<div v-for="a in list" :key="a.id" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-3" :class="{ 'opacity-50': a.is_archived }">
<div class="flex justify-between items-start">
<div class="min-w-0">
<div class="font-medium text-sm truncate">{{ a.name }}</div>
<div class="text-xs text-slate-400 mt-0.5">{{ a.provider_name || a.provider }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full shrink-0" :class="Fmt.typeBadge(a.asset_type)">{{ Fmt.TYPE_LABELS[a.asset_type] }}</span>
</div>
<div class="flex items-center gap-3 mt-2 text-xs">
<span :class="Fmt.expiryText(a.days_to_expiry)">{{ a.expiry_date || '无到期' }}<span v-if="a.days_to_expiry!==null&&a.days_to_expiry!==undefined"> ({{ a.days_to_expiry }}天)</span></span>
<span class="text-slate-500">{{ a.cost }}{{ a.currency }}</span>
<span class="px-2 py-0.5 rounded-full" :class="Fmt.statusBadge(a.status)">{{ Fmt.STATUS_LABELS[a.status] }}</span>
</div>
<div class="flex gap-4 mt-2 pt-2 border-t border-slate-100 dark:border-slate-800 text-xs">
<button v-if="a.asset_type==='vps'" @click="viewServer(a)" class="text-emerald-600 dark:text-emerald-400">监控</button>
<button v-if="a.asset_type==='ai_agent'" @click="refreshBalance(a)" class="text-violet-600 dark:text-violet-400">刷新余额</button>
<button @click="edit(a)" class="text-blue-600 dark:text-blue-400">编辑</button>
<button @click="del(a)" class="text-red-600 dark:text-red-400">删除</button>
</div>
</div>
</div>
</template>
<!-- 专有视图(域名/Cloudflare/订阅/AI账号) -->
<component v-else :is="tabView"></component>
</div>`,
setup() {
// tip 为 TAB 悬浮说明:让用户一眼明白每个分类装的是什么
const tabs = [
{ key: '', label: '全部', tip: '所有类型的资产' },
{ key: 'vps', label: 'VPS', tip: '云服务器(可查看资源监控)' },
{ key: 'domain', label: '域名', tip: '域名与解析' },
{ key: 'cloudflare', label: 'Cloudflare', tip: 'Cloudflare 账号下的资源' },
{ key: 'subscription', label: '付费资产', tip: '有费用或到期日、需要续费的项目(跨类型汇总)' },
{ key: 'ai_agent', label: 'AI账号', tip: 'ChatGPT 等 AI 服务账号与余额' },
];
const tabView = Vue.computed(() => {
const t = store.assetTab;
if (t === 'domain') return DomainsView;
if (t === 'cloudflare') return CloudflareView;
if (t === 'subscription') return SubscriptionsView;
if (t === 'ai_agent') return AIAccountsView;
return null;
});
// 通用列表:全部显示所有,VPS 前端过滤类型
const list = Vue.computed(() => {
if (store.assetTab === 'vps') return store.assets.filter(a => a.asset_type === 'vps');
return store.assets;
});
function switchTab(k) {
store.assetTab = k;
// 专有视图需要全量数据;若此前按类型过滤过则清空重载
if (k !== '' && k !== 'vps' && store.filterType) { store.filterType = ''; loadAssets(); }
}
async function refreshBalance(a) {
if (!confirm('刷新「' + a.name + '」的余额?')) return;
try {
const r = await Api.post('/assets/' + a.id + '/refresh-balance', {});
alert('余额已更新:' + r.balance + ' ' + r.currency);
await loadAssets();
} catch (e) { alert('刷新失败:' + e.message); }
}
async function syncAllAI() {
if (!confirm('同步所有 AI 账号的余额?')) return;
try {
const r = await Api.post('/sync/ai-balances', {});
alert('同步完成:成功 ' + r.success + ',失败 ' + r.failed + (r.errors && r.errors.length ? '\n' + r.errors.join('\n') : ''));
await loadAssets();
} catch (e) { alert('同步失败:' + e.message); }
}
return { store, Fmt, tabs, tabView, list, switchTab, reload: loadAssets, edit: openAssetEdit, del: deleteAsset, viewServer, refreshBalance, syncAllAI };
},
};