/* vps-manager 前端逻辑(Vue 3 全局构建) */ const { createApp, ref, reactive, onMounted } = Vue; const CFG = window.APP_CONFIG || { appName: 'VPS 资产管理系统' }; // 若服务端 .env 配置了 API_KEY,请在此填写以通过写操作鉴权 const API_KEY = ''; const TYPE_LABELS = { vps: 'VPS', domain: '域名', ai_agent: 'AI账号', cloudflare: 'Cloudflare', other: '其他' }; const STATUS_LABELS = { active: '使用中', expired: '已过期', stopped: '已停止', cancelled: '已注销', unknown: '未知' }; async function api(path, options = {}) { const headers = { 'Content-Type': 'application/json' }; if (API_KEY) headers['X-API-Key'] = API_KEY; const res = await fetch('/api' + path, { ...options, headers: { ...headers, ...(options.headers || {}) } }); if (!res.ok) { let msg = res.statusText; try { const e = await res.json(); msg = (typeof e.detail === 'string' ? e.detail : JSON.stringify(e.detail)) || msg; } catch (_) {} throw new Error(msg); } if (res.status === 204) return null; return res.json(); } function emptyForm() { return { name: '', asset_type: 'vps', provider: '', 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: '' }, domain_detail: { domain_name: '', registrar: '', dns_provider: '', cloudflare_account: '', is_using: true, redirect_target: '' }, ai_detail: { provider: '', api_key: '', plan: '', balance: null, currency: 'USD', monthly_usage: null, monthly_limit: null }, }; } // 空字符串转 null(可选字段),保留数字与布尔 function cleanDetail(obj) { const out = {}; for (const [k, v] of Object.entries(obj)) { out[k] = (v === '' ? null : v); } return out; } createApp({ setup() { const appName = CFG.appName; const assets = ref([]); const overview = ref({}); const expiring = ref([]); const loading = ref(false); const search = ref(''); const filterType = ref(''); const filterStatus = ref(''); const showModal = ref(false); const editing = ref(null); const form = reactive(emptyForm()); const error = ref(''); const typeLabels = TYPE_LABELS; const statusLabels = STATUS_LABELS; async function loadAssets() { loading.value = true; try { const params = new URLSearchParams(); if (filterType.value) params.set('asset_type', filterType.value); if (filterStatus.value) params.set('status', filterStatus.value); if (search.value) params.set('q', search.value); params.set('sort', 'expiry_date'); params.set('order', 'asc'); assets.value = await api('/assets?' + params.toString()); } catch (e) { error.value = e.message; } finally { loading.value = false; } } async function loadOverview() { try { overview.value = await api('/stats/overview'); } catch (e) { error.value = e.message; } } async function loadExpiring() { try { expiring.value = await api('/stats/expiring?days=30'); } catch (e) { error.value = e.message; } } async function loadAll() { await Promise.all([loadAssets(), loadOverview(), loadExpiring()]); } function resetForm() { Object.assign(form, emptyForm()); } function openCreate() { resetForm(); editing.value = null; showModal.value = true; } function openEdit(a) { resetForm(); editing.value = a.id; ['name', 'asset_type', 'provider', '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); showModal.value = true; } function buildPayload() { const p = { name: form.name, asset_type: form.asset_type, provider: form.provider, account: form.account || null, expiry_date: form.expiry_date || null, auto_renew: !!form.auto_renew, cost: Number(form.cost) || 0, currency: form.currency, status: form.status, is_archived: !!form.is_archived, remark: form.remark || null, }; if (form.asset_type === 'vps') p.vps_detail = cleanDetail(form.vps_detail); else if (form.asset_type === 'domain') p.domain_detail = cleanDetail(form.domain_detail); else if (form.asset_type === 'ai_agent') p.ai_detail = cleanDetail(form.ai_detail); return p; } async function saveAsset() { error.value = ''; try { const payload = buildPayload(); if (editing.value) { await api('/assets/' + editing.value, { method: 'PUT', body: JSON.stringify(payload) }); } else { await api('/assets', { method: 'POST', body: JSON.stringify(payload) }); } showModal.value = false; await loadAll(); } catch (e) { error.value = e.message; } } async function deleteAsset(a) { if (!confirm('确认删除资产「' + a.name + '」?此操作不可恢复。')) return; error.value = ''; try { await api('/assets/' + a.id, { method: 'DELETE' }); await loadAll(); } catch (e) { error.value = e.message; } } // 样式辅助 function typeBadge(t) { return { vps: 'bg-blue-100 text-blue-700', domain: 'bg-green-100 text-green-700', ai_agent: 'bg-purple-100 text-purple-700', cloudflare: 'bg-orange-100 text-orange-700', other: 'bg-slate-100 text-slate-600' }[t] || 'bg-slate-100 text-slate-600'; } function statusBadge(s) { return { active: 'bg-green-100 text-green-700', expired: 'bg-red-100 text-red-700', stopped: 'bg-slate-200 text-slate-600', cancelled: 'bg-slate-200 text-slate-500', unknown: 'bg-yellow-100 text-yellow-700' }[s] || 'bg-slate-100 text-slate-600'; } function expiryText(d) { if (d === null || d === undefined) return 'text-slate-400'; if (d < 0) return 'text-red-600 font-bold'; if (d <= 7) return 'text-red-600 font-semibold'; if (d <= 30) return 'text-amber-600'; return 'text-slate-600'; } function expiryBadge(d) { if (d < 0) return 'bg-red-100 text-red-700'; if (d <= 7) return 'bg-red-100 text-red-700'; if (d <= 30) return 'bg-amber-100 text-amber-700'; return 'bg-slate-100 text-slate-600'; } onMounted(loadAll); return { appName, assets, overview, expiring, loading, search, filterType, filterStatus, showModal, editing, form, error, typeLabels, statusLabels, loadAssets, openCreate, openEdit, saveAsset, deleteAsset, typeBadge, statusBadge, expiryText, expiryBadge, }; }, }).mount('#app');