128 lines
5.9 KiB
JavaScript
128 lines
5.9 KiB
JavaScript
/* vps-manager 前端应用入口(Vue 3 组件化 + Hash 路由) */
|
|
|
|
/* ================= 导航与 Hash 路由 ================= */
|
|
const NAVS = [
|
|
{ key: 'dashboard', label: '总览', icon: '📊' },
|
|
{ key: 'assets', label: '资产', icon: '📦' },
|
|
{ key: 'monitor', label: '监控中心', icon: '⏳' },
|
|
{ key: 'providers', label: '平台', icon: '🏢' },
|
|
{ key: 'settings', label: '设置', icon: '⚙️' },
|
|
];
|
|
const VIEW_MAP = {
|
|
dashboard: 'dashboard-view', assets: 'assets-view', monitor: 'monitor-view',
|
|
providers: 'providers-view', settings: 'settings-view',
|
|
// 服务器监控为隐藏路由(不进导航,从资产页 VPS「监控」进入)
|
|
servers: 'servers-view',
|
|
};
|
|
const VALID_VIEWS = new Set(Object.keys(VIEW_MAP));
|
|
|
|
function _viewFromHash() {
|
|
const h = (window.location.hash || '').replace(/^#\/?/, '').split('?')[0];
|
|
return VALID_VIEWS.has(h) ? h : 'dashboard';
|
|
}
|
|
|
|
function _applyView(key) {
|
|
store.view = key;
|
|
if (key !== 'servers') store.serverMetrics = {};
|
|
}
|
|
|
|
function navigate(key) {
|
|
if (!VALID_VIEWS.has(key)) key = 'dashboard';
|
|
if (window.location.hash !== '#' + key) {
|
|
window.location.hash = '#' + key;
|
|
} else {
|
|
_applyView(key);
|
|
}
|
|
}
|
|
|
|
function initRouter() {
|
|
_applyView(_viewFromHash());
|
|
window.addEventListener('hashchange', () => { _applyView(_viewFromHash()); });
|
|
}
|
|
|
|
/* ================= 根组件 ================= */
|
|
const AppRoot = {
|
|
template: `
|
|
<div class="min-h-screen md:flex">
|
|
<aside class="hidden md:flex flex-col w-52 shrink-0 border-r border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-3 sticky top-0 h-screen">
|
|
<div class="px-2 py-3 font-bold text-slate-900 dark:text-white">📦 {{ appName }}</div>
|
|
<nav class="space-y-1 mt-2">
|
|
<button v-for="n in navs" :key="n.key" @click="go(n.key)" class="w-full text-left px-3 py-2 rounded-lg text-sm flex items-center gap-2 transition" :class="store.view===n.key ? 'bg-blue-500/10 text-blue-600 dark:text-blue-400 font-medium' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800'">
|
|
<span>{{ n.icon }}</span>{{ n.label }}
|
|
</button>
|
|
</nav>
|
|
<div class="mt-auto pt-3 border-t border-slate-100 dark:border-slate-800">
|
|
<button @click="toggleDark" class="w-full text-left px-3 py-2 rounded-lg text-sm text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800">{{ store.dark ? '☀️ 浅色' : '🌙 深色' }}</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
<header class="sticky top-0 z-30 bg-white/80 dark:bg-slate-900/80 backdrop-blur border-b border-slate-200 dark:border-slate-800 px-4 py-3 flex items-center gap-3">
|
|
<h1 class="font-semibold text-slate-900 dark:text-white md:text-lg">{{ currentNav.label }}</h1>
|
|
<div class="ml-auto flex items-center gap-2">
|
|
<input v-if="store.view==='assets'" v-model="store.search" @input="reload" placeholder="搜索…" class="text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 w-32 md:w-48">
|
|
<button @click="quickAdd" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700 whitespace-nowrap">+ 新增</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div v-if="store.error" class="mx-4 mt-3 px-4 py-2.5 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-900/50 text-red-700 dark:text-red-400 text-sm flex justify-between">
|
|
<span>⚠️ {{ store.error }}</span><button @click="store.error=''" class="font-bold">✕</button>
|
|
</div>
|
|
|
|
<main class="flex-1 p-4 pb-20 md:pb-6 overflow-y-auto">
|
|
<component :is="viewComponent"></component>
|
|
</main>
|
|
</div>
|
|
|
|
<nav class="md:hidden fixed bottom-0 inset-x-0 z-30 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 flex">
|
|
<button v-for="n in navs" :key="n.key" @click="go(n.key)" class="flex-1 py-2 flex flex-col items-center gap-0.5 text-xs" :class="store.view===n.key ? 'text-blue-600 dark:text-blue-400' : 'text-slate-400'">
|
|
<span class="text-base">{{ n.icon }}</span>{{ n.label }}
|
|
</button>
|
|
</nav>
|
|
|
|
<asset-modal></asset-modal>
|
|
<provider-modal></provider-modal>
|
|
</div>`,
|
|
setup() {
|
|
const appName = Api.CFG.appName;
|
|
const navs = NAVS;
|
|
const currentNav = Vue.computed(() => navs.find(n => n.key === store.view) || navs[0]);
|
|
const viewComponent = Vue.computed(() => VIEW_MAP[store.view] || 'dashboard-view');
|
|
function go(key) { navigate(key); }
|
|
function quickAdd() {
|
|
if (store.view === 'providers') return openProviderCreate();
|
|
// 资产页:按当前 TAB 预设类型;监控中心/总览/设置:默认 VPS
|
|
let preset = 'vps';
|
|
if (store.view === 'assets') {
|
|
const t = store.assetTab;
|
|
if (t === 'ai_agent') return openAICreate('');
|
|
if (t === 'domain' || t === 'cloudflare' || t === 'subscription' || t === 'vps') preset = t === 'subscription' ? 'vps' : t;
|
|
}
|
|
openAssetCreate(preset);
|
|
}
|
|
function toggleDark() { store.dark = !store.dark; applyDark(); }
|
|
// 搜索防抖:300ms 内连续输入只触发一次请求
|
|
let searchTimer = null;
|
|
function debouncedReload() {
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => { loadAssets(); }, 300);
|
|
}
|
|
return { store, appName, navs, currentNav, viewComponent, go, quickAdd, toggleDark, reload: debouncedReload };
|
|
},
|
|
};
|
|
|
|
/* ================= 挂载 ================= */
|
|
const app = Vue.createApp(AppRoot);
|
|
app.component('dashboard-view', DashboardView);
|
|
app.component('assets-view', AssetsView);
|
|
app.component('monitor-view', MonitorView);
|
|
app.component('providers-view', ProvidersView);
|
|
app.component('servers-view', ServersView);
|
|
app.component('settings-view', SettingsView);
|
|
app.component('asset-modal', AssetModal);
|
|
app.component('provider-modal', ProviderModal);
|
|
applyDark();
|
|
initRouter();
|
|
loadAll();
|
|
app.mount('#app');
|