/* vps-manager 前端应用(Vue 3 组件化) */ const { createApp, reactive, ref, computed, onMounted, watch } = Vue; const Api = window.VpsApi; const Fmt = window.VpsFmt; /* ---------------- 全局状态 ---------------- */ const store = reactive({ view: 'dashboard', assets: [], providers: [], overview: {}, expiring: [], serverMetrics: {}, search: '', filterType: '', filterStatus: '', dark: localStorage.getItem('vps_dark') === '1', loading: false, error: '', assetModal: { show: false, editing: null, form: null }, providerModal: { show: false, editing: null, form: null }, }); function applyDark() { document.documentElement.classList.toggle('dark', store.dark); localStorage.setItem('vps_dark', store.dark ? '1' : '0'); } /* ---------------- 表单工厂 ---------------- */ function emptyAssetForm() { return { name: '', asset_type: 'vps', provider: '', provider_id: null, renewal_cycle: 'monthly', account: '', expiry_date: '', auto_renew: false, cost: 0, currency: 'USD', status: 'active', is_archived: false, remark: '', vps_detail: { ip_address: '', tailscale_ip: '', region: '', os: '', cpu_cores: 1, memory_gb: 1, disk_gb: 20, bandwidth_gb: null, ssh_port: 22, panel_url: '', ssh_user: '', login_method: 'key', ssh_key: '', password: '', purpose: '' }, domain_detail: { domain_name: '', registrar: '', dns_provider: '', cloudflare_account: '', is_using: true, redirect_target: '', bind_asset_id: null }, ai_detail: { provider: '', api_key: '', plan: '', balance: null, currency: 'USD', monthly_usage: null, monthly_limit: null }, }; } function buildAssetPayload(f) { const p = { name: f.name, asset_type: f.asset_type, provider: f.provider, provider_id: f.provider_id || null, renewal_cycle: f.renewal_cycle || null, account: f.account || null, expiry_date: f.expiry_date || null, auto_renew: !!f.auto_renew, cost: Number(f.cost) || 0, currency: f.currency, status: f.status, is_archived: !!f.is_archived, remark: f.remark || null, }; if (f.asset_type === 'vps') { const v = f.vps_detail; p.vps_detail = { ip_address: v.ip_address, tailscale_ip: v.tailscale_ip || null, region: v.region || null, os: v.os || null, cpu_cores: Number(v.cpu_cores) || 1, memory_gb: Number(v.memory_gb) || 1, disk_gb: Number(v.disk_gb) || 20, bandwidth_gb: v.bandwidth_gb ? Number(v.bandwidth_gb) : null, ssh_port: Number(v.ssh_port) || 22, panel_url: v.panel_url || null, ssh_user: v.ssh_user || null, login_method: v.login_method || 'key', ssh_key: v.ssh_key || null, password: v.password || null, purpose: v.purpose || null }; } else if (f.asset_type === 'domain') { const d = f.domain_detail; p.domain_detail = { domain_name: d.domain_name, registrar: d.registrar || null, dns_provider: d.dns_provider || null, cloudflare_account: d.cloudflare_account || null, is_using: !!d.is_using, redirect_target: d.redirect_target || null, bind_asset_id: d.bind_asset_id || null }; } else if (f.asset_type === 'ai_agent') { const a = f.ai_detail; p.ai_detail = { provider: a.provider, api_key: a.api_key || null, plan: a.plan || null, balance: a.balance ? Number(a.balance) : null, currency: a.currency || 'USD', monthly_usage: a.monthly_usage ? Number(a.monthly_usage) : null, monthly_limit: a.monthly_limit ? Number(a.monthly_limit) : null }; } return p; } /* ---------------- 数据加载 ---------------- */ async function loadAssets() { const params = new URLSearchParams(); if (store.filterType) params.set('asset_type', store.filterType); if (store.filterStatus) params.set('status', store.filterStatus); if (store.search) params.set('q', store.search); params.set('sort', 'expiry_date'); params.set('order', 'asc'); store.assets = await Api.get('/assets?' + params.toString()); } async function loadProviders() { store.providers = await Api.get('/providers'); } async function loadOverview() { store.overview = await Api.get('/stats/overview'); } async function loadExpiring() { store.expiring = await Api.get('/stats/expiring?days=30'); } async function loadAll() { store.loading = true; store.error = ''; try { await Promise.all([loadAssets(), loadProviders(), loadOverview(), loadExpiring()]); } catch (e) { store.error = e.message; } finally { store.loading = false; } } /* ---------------- 资产操作 ---------------- */ function openAssetCreate(presetType) { const form = emptyAssetForm(); if (presetType) form.asset_type = presetType; store.assetModal = { show: true, editing: null, form }; } function openAssetEdit(a) { 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]; }); if (a.vps_detail) Object.assign(form.vps_detail, a.vps_detail); if (a.domain_detail) Object.assign(form.domain_detail, a.domain_detail); if (a.ai_detail) Object.assign(form.ai_detail, a.ai_detail); form.vps_detail.ssh_key = ''; form.vps_detail.password = ''; store.assetModal = { show: true, editing: a.id, form }; } async function saveAsset() { store.error = ''; const f = store.assetModal.form; try { const payload = buildAssetPayload(f); if (store.assetModal.editing) await Api.put('/assets/' + store.assetModal.editing, payload); else await Api.post('/assets', payload); store.assetModal.show = false; await loadAll(); } catch (e) { store.error = e.message; } } async function deleteAsset(a) { if (!confirm('确认删除资产「' + a.name + '」?此操作不可恢复。')) return; store.error = ''; try { await Api.del('/assets/' + a.id); await loadAll(); } catch (e) { store.error = e.message; } } /* ---------------- 平台操作 ---------------- */ function openProviderCreate() { store.providerModal = { show: true, editing: null, form: { slug: '', name: '', name_en: '', category: 'vps', website: '', console_url: '', sdk_type: '', enabled: true, remark: '', api_config: '' } }; } function openProviderEdit(p) { store.providerModal = { show: true, editing: p.id, form: { slug: p.slug, name: p.name, name_en: p.name_en || '', category: p.category, website: p.website || '', console_url: p.console_url || '', sdk_type: p.sdk_type || '', enabled: p.enabled, remark: p.remark || '', api_config: '' } }; } async function saveProvider() { store.error = ''; const f = store.providerModal.form; const payload = { slug: f.slug, name: f.name, name_en: f.name_en || null, category: f.category, website: f.website || null, console_url: f.console_url || null, sdk_type: f.sdk_type || null, enabled: !!f.enabled, remark: f.remark || null, api_config: f.api_config || null }; try { if (store.providerModal.editing) await Api.put('/providers/' + store.providerModal.editing, payload); else await Api.post('/providers', payload); store.providerModal.show = false; await loadProviders(); } catch (e) { store.error = e.message; } } async function seedProviders() { store.error = ''; try { const r = await Api.post('/providers/seed', {}); await loadProviders(); alert('已初始化预设平台,新增 ' + r.added + ' 个'); } catch (e) { store.error = e.message; } } async function deleteProvider(p) { if (!confirm('确认删除平台「' + p.name + '」?')) return; try { await Api.del('/providers/' + p.id); await loadProviders(); } catch (e) { store.error = e.message; } } /* ---------------- 服务器监控 ---------------- */ async function viewServer(a) { store.view = 'servers'; try { const [metrics, info, checks] = await Promise.all([ Api.get('/monitor/' + a.id + '/metrics'), Api.get('/monitor/' + a.id + '/info'), Api.get('/monitor/' + a.id + '/security'), ]); store.serverMetrics = { asset: a, metrics, info, checks }; } catch (e) { store.serverMetrics = { asset: a, metrics: [], info: null, checks: [] }; } } /* ---------------- 设置 ---------------- */ const settings = reactive({ apiKey: Api.getApiKey(), saved: false }); function saveSettings() { Api.setApiKey(settings.apiKey); settings.saved = true; setTimeout(() => settings.saved = false, 2000); } /* ================= 视图组件 ================= */ const DashboardView = { template: `
总资产
{{ store.overview.total || 0 }}
30天内到期
{{ store.overview.expiring_30 || 0 }}
本年支出
{{ store.overview.year_cost || 0 }}
异常状态
{{ store.overview.abnormal_count || 0 }}

续费倒计时(30天内)

{{ a.name }} {{ a.provider_name || a.provider }}
{{ a.days_to_expiry }}天 · {{ a.cost }}{{ a.currency }}

按类型分布

{{ Fmt.TYPE_LABELS[t] }} {{ n }} 暂无数据
`, setup() { return { store, Fmt }; }, }; const AssetsView = { template: `
共 {{ store.assets.length }} 条
暂无资产
{{ a.name }}
{{ a.provider_name || a.provider }}
{{ Fmt.TYPE_LABELS[a.asset_type] }}
{{ a.expiry_date || '无到期' }} ({{ a.days_to_expiry }}天) {{ a.cost }}{{ a.currency }} {{ Fmt.STATUS_LABELS[a.status] }}
`, setup() { return { store, Fmt, reload: loadAssets, edit: openAssetEdit, del: deleteAsset, viewServer }; }, }; const ProvidersView = { template: `
{{ p.name }}
{{ p.slug }}
{{ Fmt.CATEGORY_LABELS[p.category] }}
SDK: {{ p.sdk_type }}
管理面板 ↗
已配置 API 未配置 API {{ p.enabled ? '启用' : '停用' }}
`, setup() { return { store, Fmt, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider }; }, }; const ServersView = { template: `
请在「资产」页点击 VPS 的「监控」查看服务器资源与安全状态
`, setup() { const m = computed(() => store.serverMetrics); const latest = computed(() => (m.value.metrics && m.value.metrics.length) ? m.value.metrics[0] : null); return { store, Fmt, m, latest }; }, }; const DomainsView = { template: `
域名注册商DNS到期使用中操作
暂无域名资产
{{ a.domain_detail.domain_name }} {{ a.domain_detail.registrar || '—' }} {{ a.domain_detail.dns_provider || '—' }} {{ a.expiry_date || '—' }}({{ a.days_to_expiry }}天) {{ a.domain_detail.is_using ? '使用' : '闲置' }}
`, setup() { const domains = computed(() => store.assets.filter(a => a.asset_type === 'domain')); return { store, Fmt, domains, edit: openAssetEdit }; }, }; const SettingsView = { template: `

API Key(写操作鉴权)

若服务端 .env 配置了 API_KEY,请在此填写相同的值,前端写操作会自动携带。

外观

`, setup() { return { store, settings, save: saveSettings, applyDark }; }, }; /* ================= 资产编辑模态框 ================= */ const AssetModal = { template: `

{{ store.assetModal.editing ? '编辑资产' : '新增资产' }}

VPS 详情
域名详情
AI 账号详情
`, setup() { const f = computed(() => store.assetModal.form); const providersForType = computed(() => { const t = f.value ? f.value.asset_type : null; const map = { vps: 'vps', domain: 'domain', ai_agent: 'ai_agent', cloudflare: 'cloudflare' }; const cat = map[t]; return cat ? store.providers.filter(p => p.category === cat || p.category === 'other') : store.providers; }); function onProviderChange() { const p = store.providers.find(x => x.id === f.value.provider_id); if (p) f.value.provider = p.slug; } return { store, Fmt, f, providersForType, onProviderChange, save: saveAsset }; }, }; const ProviderModal = { template: `

{{ store.providerModal.editing ? '编辑平台' : '新增平台' }}

`, setup() { return { store, Fmt, f: computed(() => store.providerModal.form), save: saveProvider }; }, }; /* ================= 根组件 ================= */ const AppRoot = { components: { DashboardView, AssetsView, ProvidersView, ServersView, DomainsView, SettingsView, AssetModal, ProviderModal }, template: `

{{ currentNav.label }}

⚠️ {{ store.error }}
`, setup() { const appName = Api.CFG.appName; const navs = [ { key: 'dashboard', label: '总览', icon: '📊' }, { key: 'assets', label: '资产', icon: '📦' }, { key: 'servers', label: '服务器', icon: '🖥️' }, { key: 'domains', label: '域名', icon: '🌐' }, { key: 'providers', 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 currentNav = computed(() => navs.find(n => n.key === store.view) || navs[0]); const viewComponent = computed(() => viewMap[store.view] || 'dashboard-view'); function go(key) { store.view = key; if (key !== 'servers') store.serverMetrics = {}; } function quickAdd() { const preset = store.view === 'domains' ? 'domain' : (store.view === 'providers' ? null : 'vps'); if (store.view === 'providers') openProviderCreate(); else openAssetCreate(preset); } function toggleDark() { store.dark = !store.dark; applyDark(); } return { store, appName, navs, currentNav, viewComponent, go, quickAdd, toggleDark, reload: loadAssets }; }, }; /* ================= 挂载 ================= */ const app = createApp(AppRoot); app.component('dashboard-view', DashboardView); app.component('assets-view', AssetsView); app.component('providers-view', ProvidersView); app.component('servers-view', ServersView); app.component('domains-view', DomainsView); app.component('settings-view', SettingsView); applyDark(); loadAll(); app.mount('#app');