158 lines
6.8 KiB
JavaScript
158 lines
6.8 KiB
JavaScript
/* 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');
|