/* vps-manager 前端应用(Vue 3 组件化) */
const { createApp, reactive, ref, computed, onMounted, watch, nextTick } = 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', renew_url: '', cancel_url: '',
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,
renew_url: f.renew_url || null, cancel_url: f.cancel_url || 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 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) {
const form = emptyAssetForm();
['name', 'asset_type', 'provider', 'provider_id', 'renewal_cycle', 'renew_url', 'cancel_url', '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?limit=200'),
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 }} |
{{ Fmt.TYPE_LABELS[a.asset_type] }} |
{{ a.provider_name || a.provider }} |
{{ a.expiry_date || '—' }}({{ a.days_to_expiry }}天) |
{{ a.cost }} {{ a.currency }}/{{ Fmt.CYCLE_LABELS[a.renewal_cycle] || a.renewal_cycle }} |
{{ Fmt.STATUS_LABELS[a.status] }} |
|
暂无资产
{{ 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() {
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, reload: loadAssets, edit: openAssetEdit, del: deleteAsset, viewServer, refreshBalance, syncAllAI };
},
};
function normalizedMonthly(cost, cycle) {
if (!cost) return 0;
if (cycle === 'yearly') return cost / 12;
if (cycle === 'quarterly') return cost / 3;
return cost;
}
const SubscriptionsView = {
template: `
月度支出({{ cur }})
{{ v.toFixed(2) }}
年度约 {{ (v*12).toFixed(2) }}
按平台月度支出(换算为月均)
| 订阅 |
平台 |
费用 |
到期 |
管理 |
| 暂无付费订阅(给资产设置费用和到期日即可出现) |
| {{ a.name }} |
{{ a.provider_name || a.provider }} |
{{ a.cost }} {{ a.currency }}/{{ Fmt.CYCLE_LABELS[a.renewal_cycle] || a.renewal_cycle }} |
{{ a.expiry_date || '—' }}({{ a.days_to_expiry }}天) |
续费
取消
—
|
`,
setup() {
const subs = computed(() => store.assets
.filter(a => (a.cost > 0 || a.expiry_date) && a.status !== 'cancelled')
.sort((x, y) => (x.days_to_expiry ?? 9999) - (y.days_to_expiry ?? 9999)));
const monthlyByCurrency = computed(() => {
const m = {};
for (const a of subs.value) {
const cur = a.currency || 'USD';
m[cur] = (m[cur] || 0) + normalizedMonthly(a.cost, a.renewal_cycle);
}
return m;
});
const byProviderMonthly = computed(() => {
const m = {};
for (const a of subs.value) {
const prov = a.provider_name || a.provider;
m[prov] = (m[prov] || 0) + normalizedMonthly(a.cost, a.renewal_cycle);
}
return m;
});
let billingChart = null;
function renderBillingChart() {
const canvas = document.getElementById('billingChart');
if (!canvas || !window.Chart) return;
const entries = Object.entries(byProviderMonthly.value).sort((x, y) => y[1] - x[1]);
if (!entries.length) return;
if (billingChart) { billingChart.destroy(); billingChart = null; }
billingChart = new Chart(canvas, {
type: 'bar',
data: {
labels: entries.map(e => e[0]),
datasets: [{ label: '月度支出', data: entries.map(e => e[1].toFixed(2)), backgroundColor: '#6366f1', borderRadius: 6 }],
},
options: {
responsive: true,
scales: { y: { beginAtZero: true } },
plugins: { legend: { display: false } },
},
});
}
watch([subs, byProviderMonthly], () => { nextTick(renderBillingChart); }, { deep: true });
onMounted(() => { nextTick(renderBillingChart); });
return { store, Fmt, subs, monthlyByCurrency, byProviderMonthly };
},
};
const ProvidersView = {
template: `
{{ p.name }}
{{ p.slug }}
{{ Fmt.CATEGORY_LABELS[p.category] }}
SDK: {{ p.sdk_type }}
已配置 API
未配置 API
{{ p.enabled ? '启用' : '停用' }}
最后同步:{{ fmtTime(p.last_synced_at) }}
{{ results[p.id].text }}
`,
setup() {
const testing = reactive({});
const syncing = reactive({});
const results = reactive({});
const supported = ref([]);
const capabilities = ref({});
onMounted(async () => {
try { const r = await Api.get('/providers/adapters'); supported.value = r.supported_types || []; capabilities.value = r.capabilities || {}; } catch (e) { /* ignore */ }
});
function isSupported(t) { return !!t && supported.value.includes(t); }
function canSync(t) { const c = capabilities.value[t]; return !!(c && (c.list_vps || c.list_domains)); }
function fmtTime(iso) {
if (!iso) return '';
return new Date(iso).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
async function test(p) {
testing[p.id] = true; delete results[p.id];
try {
const r = await Api.post('/providers/' + p.id + '/test', {});
results[p.id] = { ok: r.ok, text: (r.ok ? '✓ ' : '✗ ') + r.message };
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
finally { testing[p.id] = false; }
}
async function sync(p) {
syncing[p.id] = true; delete results[p.id];
try {
const r = await Api.post('/providers/' + p.id + '/sync', {});
const parts = [];
if (r.vps) parts.push('VPS 新增' + r.vps.created + '/更新' + r.vps.updated);
if (r.domains) parts.push('域名 新增' + r.domains.created + '/更新' + r.domains.updated);
if (r.vps_error) parts.push('VPS错误:' + r.vps_error);
if (r.domains_error) parts.push('域名错误:' + r.domains_error);
if (r.account && r.account.balance !== null && r.account.balance !== undefined) parts.push('余额 ' + r.account.balance);
results[p.id] = { ok: true, text: '✓ 同步完成:' + (parts.join(',') || '无变化') };
await loadAll();
} catch (e) { results[p.id] = { ok: false, text: '✗ ' + e.message }; }
finally { syncing[p.id] = false; }
}
return { store, Fmt, testing, syncing, results, isSupported, canSync, fmtTime, test, sync, seed: seedProviders, create: openProviderCreate, edit: openProviderEdit, del: deleteProvider };
},
};
const ServersView = {
template: `
请在「资产」页点击 VPS 的「监控」查看服务器资源与安全状态
{{ m.asset.name }}
CPU
{{ latest ? latest.cpu_pct : '—' }}%
{{ m.info.cpu_cores }} 核
内存
{{ latest ? latest.mem_pct : '—' }}%
{{ m.info.mem_total_gb }} GB
磁盘
{{ latest ? latest.disk_pct : '—' }}%
{{ m.info.disk_total_gb }} GB
负载/在线
{{ latest && latest.load_1m !== null ? latest.load_1m : '—' }}
{{ m.info.hostname }}
服务器信息
系统:{{ m.info.os }}
内核:{{ m.info.kernel }}
公网 IP:{{ m.info.public_ip || '—' }}
状态:{{ m.info.status }}
资源趋势(最近 {{ m.metrics.length }} 次采样)
安全检查
{{ c.status }}
{{ c.check_item }}
{{ c.detail }}
建议:{{ c.suggestion }}
暂无安全检查数据(需 Agent 上报)
`,
setup() {
const m = computed(() => store.serverMetrics);
const latest = computed(() => (m.value.metrics && m.value.metrics.length) ? m.value.metrics[0] : null);
let chartInstance = null;
function renderChart() {
const canvas = document.getElementById('metricsChart');
if (!canvas || !window.Chart) return;
if (!m.value.metrics || m.value.metrics.length < 2) return;
if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
const data = [...m.value.metrics].reverse();
const labels = data.map(p => new Date(p.ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }));
chartInstance = new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [
{ label: 'CPU %', data: data.map(p => p.cpu_pct), borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.1)', tension: 0.3, pointRadius: 0 },
{ label: '内存 %', data: data.map(p => p.mem_pct), borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,0.1)', tension: 0.3, pointRadius: 0 },
{ label: '磁盘 %', data: data.map(p => p.disk_pct), borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.1)', tension: 0.3, pointRadius: 0 },
],
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
scales: { y: { min: 0, max: 100, ticks: { callback: v => v + '%' } } },
plugins: { legend: { position: 'top' } },
},
});
}
watch(m, () => { nextTick(renderChart); }, { deep: true });
onMounted(() => { nextTick(renderChart); });
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 AIAccountsView = {
template: `
共 {{ totalAccounts }} 个账号 · 总余额 {{ totalBalance }}
{{ g.provider }}
{{ g.accounts.length }} 个账号
余额合计 {{ g.totalBalance }}
{{ a.name }}
{{ a.ai_detail.plan }}
余额 {{ 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 : '' }}
用量 {{ a.ai_detail.monthly_usage }}/{{ a.ai_detail.monthly_limit ?? '∞' }}
暂无 AI 账号,点右上角「新增账号」添加(同一平台可添加多个)
`,
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 = {
template: `
通知与续费提醒
已配置渠道:
{{ notifyChannels.join('、') }}
未配置(在 .env 设置 TELEGRAM_*/SMTP_*)
{{ notifyResult }}
外观
`,
setup() {
const notifyChannels = ref([]);
const notifyResult = ref('');
onMounted(async () => {
try { const r = await Api.get('/notify/channels'); notifyChannels.value = r.channels || []; } catch (e) { /* ignore */ }
});
async function testNotify() {
notifyResult.value = '发送中…';
try {
const r = await Api.post('/notify/test', {});
notifyResult.value = r.notifications.map(n => '[' + n.channel + '] ' + (n.ok ? '成功' : '失败:' + n.message)).join('\n');
} catch (e) { notifyResult.value = '失败:' + e.message; }
}
async function checkRenewals() {
notifyResult.value = '检查中…';
try {
const r = await Api.post('/notify/renewals?send=true', {});
let text = '即将到期 ' + r.expiring_count + ' 项(阈值 ' + r.threshold_days + ' 天)';
if (r.expiring.length) text += '\n' + r.expiring.map(i => '• ' + i.name + ' [' + i.provider + '] ' + i.days + ' 天后').join('\n');
if (r.notifications && r.notifications.length) text += '\n\n通知:' + r.notifications.map(n => '[' + n.channel + '] ' + (n.ok ? '成功' : '失败:' + n.message)).join(',');
notifyResult.value = text;
} catch (e) { notifyResult.value = '失败:' + e.message; }
}
return { store, settings, save: saveSettings, applyDark, notifyChannels, notifyResult, testNotify, checkRenewals };
},
};
/* ================= 资产编辑模态框 ================= */
const AssetModal = {
template: `
{{ store.assetModal.editing ? '编辑资产' : '新增资产' }}
`,
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() {
const f = computed(() => store.providerModal.form);
const adapterMeta = ref([]);
const apiFields = reactive({});
onMounted(async () => {
try { const r = await Api.get('/providers/adapters'); adapterMeta.value = r.adapters || []; } catch (e) { /* ignore */ }
});
const sdkOptions = computed(() => {
const opts = [];
for (const m of adapterMeta.value) for (const t of (m.sdk_types || [])) opts.push({ type: t, fields: m.required_config || [] });
return opts;
});
const currentFields = computed(() => {
const opt = sdkOptions.value.find(o => o.type === (f.value && f.value.sdk_type));
return opt ? opt.fields : [];
});
function save() {
const cfg = {};
for (const field of currentFields.value) if (apiFields[field]) cfg[field] = apiFields[field];
if (Object.keys(cfg).length) f.value.api_config = JSON.stringify(cfg);
saveProvider();
}
return { store, Fmt, f, sdkOptions, currentFields, apiFields, save };
},
};
/* ================= 根组件 ================= */
const AppRoot = {
components: { DashboardView, AssetsView, ProvidersView, ServersView, DomainsView, SettingsView, AssetModal, ProviderModal },
template: `
`,
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: 'subscriptions', label: '订阅', icon: '💳' },
{ key: 'ai', label: 'AI账号', icon: '🤖' },
{ key: 'providers', label: '平台', icon: '🏢' },
{ key: 'settings', label: '设置', icon: '⚙️' },
];
const viewMap = { dashboard: 'dashboard-view', assets: 'assets-view', servers: 'servers-view', domains: 'domains-view', subscriptions: 'subscriptions-view', ai: 'ai-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 = {}; if (key === 'ai') { store.filterType = ''; store.filterStatus = ''; store.search = ''; loadAssets(); } }
function quickAdd() {
if (store.view === 'providers') return openProviderCreate();
if (store.view === 'ai') return openAICreate('');
const preset = store.view === 'domains' ? 'domain' : 'vps';
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('subscriptions-view', SubscriptionsView);
app.component('ai-view', AIAccountsView);
app.component('settings-view', SettingsView);
applyDark();
loadAll();
app.mount('#app');