2026-05-22 08:37:01 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN" x-data="installWizard()" x-init="init()">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>Nexus 6.0 安装向导</title>
|
2026-05-23 15:26:56 +08:00
|
|
|
|
<script src="/app/vendor/tailwindcss-browser.js"></script>
|
2026-05-31 04:11:17 +08:00
|
|
|
|
<script src="/app/vendor/theme-init.js"></script>
|
2026-05-31 02:55:45 +08:00
|
|
|
|
<link rel="stylesheet" href="/app/vendor/theme.css">
|
2026-05-23 15:26:56 +08:00
|
|
|
|
<script defer src="/app/vendor/alpinejs.min.js"></script>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
<style type="text/tailwindcss">
|
|
|
|
|
|
@theme {
|
2026-05-31 04:18:55 +08:00
|
|
|
|
--color-brand: oklch(55% 0.2 250);
|
|
|
|
|
|
--color-brand-light: oklch(75% 0.15 250);
|
|
|
|
|
|
--color-brand-dark: oklch(35% 0.18 250);
|
2026-05-22 08:37:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.installer-card { max-width: 780px; }
|
|
|
|
|
|
.code-block {
|
2026-05-31 04:18:55 +08:00
|
|
|
|
background: #1e293b; color: #e2e8f0; border-radius: 6px; padding: 12px;
|
|
|
|
|
|
font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all;
|
|
|
|
|
|
position: relative; margin: 8px 0;
|
2026-05-22 08:37:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
.copy-btn {
|
2026-05-31 04:18:55 +08:00
|
|
|
|
position: absolute; top: 6px; right: 6px;
|
|
|
|
|
|
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
|
|
|
|
|
|
color: #94a3b8; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
|
2026-05-22 08:37:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
.copy-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
|
|
|
|
|
|
.copy-btn.copied { background: #22c55e; color: #fff; border-color: #22c55e; }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<body class="bg-[var(--bg-page)] min-h-screen flex items-center justify-center p-4">
|
2026-05-22 08:37:01 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="installer-card w-full bg-white rounded-2xl shadow-2xl overflow-hidden">
|
|
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<!-- Header -->
|
|
|
|
|
|
<div class="bg-gradient-to-br from-blue-700 to-violet-600 text-white px-8 py-6 text-center">
|
|
|
|
|
|
<h1 class="text-2xl font-bold mb-1">Nexus 6.0 安装向导</h1>
|
|
|
|
|
|
<p class="text-violet-200 text-sm">服务器运维管理平台 — 心跳监控 + 智能告警 + 3层守护</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step Indicator -->
|
|
|
|
|
|
<div class="flex items-center justify-center gap-1 py-4 px-8">
|
|
|
|
|
|
<template x-for="i in 5" :key="i">
|
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<div class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all"
|
|
|
|
|
|
:class="step > i ? 'bg-green-500 text-white' : step === i ? 'bg-blue-500 text-white' : 'border-2 border-slate-200 text-[var(--text-secondary)]'"
|
|
|
|
|
|
x-text="step > i ? '✓' : i"></div>
|
|
|
|
|
|
<div x-show="i < 5" class="w-8 h-0.5 mx-0.5 transition-all"
|
|
|
|
|
|
:class="step > i ? 'bg-green-500' : 'bg-slate-200'"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Body -->
|
|
|
|
|
|
<div class="px-8 pb-8">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Error / Success alerts -->
|
|
|
|
|
|
<div x-show="error" x-transition class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-sm" x-text="error"></div>
|
|
|
|
|
|
<div x-show="success" x-transition class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg mb-4 text-sm" x-text="success"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Already installed -->
|
|
|
|
|
|
<template x-if="installed">
|
|
|
|
|
|
<div class="text-center py-8">
|
|
|
|
|
|
<div class="text-5xl mb-4">⚠️</div>
|
|
|
|
|
|
<h2 class="text-xl font-bold text-red-600 mb-2">系统已安装</h2>
|
|
|
|
|
|
<p class="text-[var(--text-muted)] mb-4">检测到 .env 配置文件,系统已完成安装。</p>
|
|
|
|
|
|
<a href="/app/login.html" class="inline-flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-5 py-2.5 rounded-lg font-medium transition">前往登录 →</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ======== Step 1: Welcome ======== -->
|
|
|
|
|
|
<template x-if="step === 1 && !installed">
|
|
|
|
|
|
<div class="text-center py-4">
|
|
|
|
|
|
<h2 class="text-xl font-bold text-slate-800 mb-4">欢迎使用 Nexus 6.0</h2>
|
|
|
|
|
|
<p class="text-[var(--text-muted)] mb-6 leading-relaxed">
|
|
|
|
|
|
本向导将引导您完成系统安装配置。<br>
|
|
|
|
|
|
安装完成后将自动生成:<code class="bg-slate-100 px-1.5 py-0.5 rounded text-sm">.env</code> (Python后端) +
|
|
|
|
|
|
<code class="bg-slate-100 px-1.5 py-0.5 rounded text-sm">config.json</code> (共享配置) +
|
|
|
|
|
|
<code class="bg-slate-100 px-1.5 py-0.5 rounded text-sm">MySQL settings表</code> (共享配置)<br>
|
|
|
|
|
|
整个过程大约 3 分钟。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="bg-slate-50 rounded-lg p-5 text-left mb-6">
|
|
|
|
|
|
<h3 class="font-semibold text-slate-700 mb-3">📋 安装前准备</h3>
|
|
|
|
|
|
<ul class="text-[var(--text-dim)] text-sm ml-5 list-disc space-y-1">
|
|
|
|
|
|
<li>Python 3.12+ (FastAPI + uvicorn)</li>
|
|
|
|
|
|
<li>MySQL 8.0+ (需提前创建数据库和用户)</li>
|
|
|
|
|
|
<li>Redis 6+ (心跳缓冲和实时数据)</li>
|
|
|
|
|
|
<li>Web 目录和根目录可写权限</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button @click="step = 2; checkEnv()" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2.5 rounded-lg font-semibold transition">
|
|
|
|
|
|
开始安装 →
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ======== Step 2: Environment Check ======== -->
|
|
|
|
|
|
<template x-if="step === 2 && !installed">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="text-lg font-bold text-slate-800 mb-4">🔍 环境检测</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div x-show="envLoading" class="text-center py-8 text-[var(--text-secondary)]">
|
|
|
|
|
|
<div class="animate-spin inline-block w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full mb-2"></div>
|
|
|
|
|
|
<p>正在检测环境...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div x-show="!envLoading && envChecks.length" class="bg-slate-50 rounded-lg overflow-hidden mb-4">
|
|
|
|
|
|
<template x-for="c in envChecks" :key="c.name">
|
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 last:border-0">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="font-medium text-slate-700" x-text="c.name"></span>
|
|
|
|
|
|
<span class="text-[var(--text-secondary)] text-xs ml-2" x-text="'需要: ' + c.required"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="font-semibold text-sm" :class="c.pass ? 'text-green-500' : 'text-red-500'"
|
|
|
|
|
|
x-text="c.pass ? '✓ 通过' : '✗ ' + c.current"></span>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div x-show="!envLoading && envAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg mb-4 text-sm">
|
2026-06-06 04:32:47 +08:00
|
|
|
|
✓ 环境检测通过。Redis 仅检测是否已安装;MySQL/Redis <b>连接</b>在步骤 4 验证。
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!envLoading && !envAllPass && envChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-sm">
|
2026-06-06 04:32:47 +08:00
|
|
|
|
部分必检项未通过,请先解决(Redis 未安装可先继续,但步骤 4 须连通)。
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div x-show="!envLoading" class="flex gap-3 mt-4">
|
2026-06-06 02:51:52 +08:00
|
|
|
|
<button @click="step = 3"
|
|
|
|
|
|
:disabled="!envAllPass"
|
|
|
|
|
|
:class="envAllPass ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-300 cursor-not-allowed'"
|
|
|
|
|
|
class="text-white px-5 py-2.5 rounded-lg font-semibold transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
下一步:数据库配置 →
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="checkEnv()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">
|
|
|
|
|
|
重新检测
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ======== Step 3: DB + Redis + API Config ======== -->
|
|
|
|
|
|
<template x-if="step === 3 && !installed">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="text-lg font-bold text-slate-800 mb-4">⚙ 数据库 + Redis + API 配置</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg mb-4 text-sm">
|
|
|
|
|
|
<b>说明:</b>请提前创建好数据库和用户,安装向导将自动建表并写入配置。
|
|
|
|
|
|
SECRET_KEY 和 API_KEY 将自动生成,安装后不可修改(加密一致性)。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form @submit.prevent="initDb()">
|
|
|
|
|
|
<!-- MySQL -->
|
|
|
|
|
|
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-3">🗄 MySQL 数据库</h3>
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">主机</label>
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<input x-model="form.db_host" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">端口</label>
|
|
|
|
|
|
<input x-model="form.db_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<div class="grid grid-cols-2 gap-4 mt-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">数据库名</label>
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<input x-model="form.db_name" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">用户名</label>
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<input x-model="form.db_user" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">密码</label>
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<input x-model="form.db_pass" @input="invalidateCredCheck()" type="password" required placeholder="数据库密码"
|
2026-05-31 04:18:55 +08:00
|
|
|
|
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Redis -->
|
|
|
|
|
|
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-3">⚡ Redis 配置</h3>
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 主机</label>
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<input x-model="form.redis_host" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 端口</label>
|
|
|
|
|
|
<input x-model="form.redis_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<div class="grid grid-cols-2 gap-4 mt-3">
|
|
|
|
|
|
<div>
|
2026-06-06 07:15:57 +08:00
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 用户名(可选)</label>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel 多数<strong>留空</strong>(仅密码);不行再填 <code class="bg-slate-100 px-1 rounded">default</code>。勿填 root</p>
|
|
|
|
|
|
<input x-model="form.redis_user" @input="invalidateCredCheck()" placeholder="通常留空"
|
2026-06-06 06:59:45 +08:00
|
|
|
|
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-06-06 06:59:45 +08:00
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
|
|
|
|
|
|
<input x-model="form.redis_db" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-06-06 06:59:45 +08:00
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码</label>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel → Redis → 参数中的密码;无密码留空</p>
|
|
|
|
|
|
<input x-model="form.redis_pass" @input="invalidateCredCheck()" type="password" placeholder="无密码留空"
|
|
|
|
|
|
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- API + Site + Timezone -->
|
|
|
|
|
|
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-3">🌐 网站地址 + API 服务 + 时区</h3>
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">网站地址</label>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-1">自动检测 — 用于 Agent 上报和前端 API 调用,可手动修改</p>
|
|
|
|
|
|
<input x-model="form.site_url" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-4 mt-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Python API 端口</label>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-1">uvicorn 监听端口</p>
|
|
|
|
|
|
<input x-model="form.api_port" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">时区</label>
|
|
|
|
|
|
<select x-model="form.timezone" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
<option value="Asia/Shanghai">Asia/Shanghai (中国标准时间)</option>
|
|
|
|
|
|
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
|
|
|
|
|
<option value="America/New_York">America/New_York</option>
|
|
|
|
|
|
<option value="Europe/London">Europe/London</option>
|
|
|
|
|
|
<option value="UTC">UTC</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<!-- Step 3: credential check before init -->
|
|
|
|
|
|
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-2">🔗 MySQL / Redis 账号检测</h3>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-2">须先检测通过,才能初始化数据库(避免账号密码错误仍写入 .env)。</p>
|
|
|
|
|
|
<div x-show="credLoading" class="text-sm text-[var(--text-secondary)] py-2">正在检测连接...</div>
|
|
|
|
|
|
<div x-show="!credLoading && credChecks.length" class="bg-slate-50 rounded-lg overflow-hidden mb-2">
|
|
|
|
|
|
<template x-for="c in credChecks" :key="c.name">
|
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 last:border-0 text-sm">
|
|
|
|
|
|
<span class="font-medium text-slate-700" x-text="c.name"></span>
|
|
|
|
|
|
<span class="font-semibold text-right max-w-[70%]" :class="c.pass ? 'text-green-500' : 'text-red-500'" x-text="c.current"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!credLoading && credAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg text-sm mb-2">
|
|
|
|
|
|
✓ MySQL 与 Redis 账号密码正确,可以初始化。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!credLoading && !credAllPass && credChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg text-sm mb-2">
|
|
|
|
|
|
连接未通过:请核对主机、用户名与密码后重新检测。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" @click="testCredentials()" :disabled="credLoading || !form.db_pass"
|
|
|
|
|
|
class="bg-slate-200 hover:bg-slate-300 disabled:opacity-50 text-slate-700 px-4 py-2 rounded-lg text-sm font-semibold transition">
|
|
|
|
|
|
检测 MySQL / Redis 连接
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<div class="bg-amber-50 border border-amber-200 text-amber-800 px-4 py-3 rounded-lg mt-4 text-sm">
|
|
|
|
|
|
<b>⚠ 注意:</b>初始化将自动:① 连接数据库并建表 ② 生成 SECRET_KEY/API_KEY
|
|
|
|
|
|
③ 写入 .env + config.json + settings表 ④ 自动计算连接池大小
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex gap-3 mt-5">
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<button type="submit" :disabled="loading || !credAllPass" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<span x-show="loading" class="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full"></span>
|
2026-06-06 06:47:22 +08:00
|
|
|
|
<span x-text="loading ? '初始化中...' : (credAllPass ? '初始化数据库 →' : '请先检测连接')"></span>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" @click="step = 2" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">← 上一步</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ======== Step 4: Admin + Brand ======== -->
|
|
|
|
|
|
<template x-if="step === 4 && !installed">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="text-lg font-bold text-slate-800 mb-4">👤 管理员账号 + 系统名称</h2>
|
|
|
|
|
|
|
2026-06-06 04:32:47 +08:00
|
|
|
|
<!-- Step 4: MySQL + Redis connection -->
|
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-2">🔗 MySQL / Redis 连接检测</h3>
|
|
|
|
|
|
<div x-show="connLoading" class="text-sm text-[var(--text-secondary)] py-2">正在检测连接...</div>
|
|
|
|
|
|
<div x-show="!connLoading && connChecks.length" class="bg-slate-50 rounded-lg overflow-hidden mb-2">
|
|
|
|
|
|
<template x-for="c in connChecks" :key="c.name">
|
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 last:border-0 text-sm">
|
|
|
|
|
|
<span class="font-medium text-slate-700" x-text="c.name"></span>
|
|
|
|
|
|
<span class="font-semibold" :class="c.pass ? 'text-green-500' : 'text-red-500'" x-text="c.current"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!connLoading && connAllPass" class="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg text-sm mb-2">
|
|
|
|
|
|
✓ MySQL 与 Redis 均已连通,可创建管理员。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!connLoading && !connAllPass && connChecks.length" class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg text-sm mb-2">
|
2026-06-06 06:47:22 +08:00
|
|
|
|
连接未通过:若步骤 3 已初始化,请在服务器修正 /app/.env 中 NEXUS_DATABASE_URL / NEXUS_REDIS_URL 后重启容器,再点重新检测。
|
2026-06-06 04:32:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" @click="checkConnections()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-4 py-2 rounded-lg text-sm font-semibold transition">
|
|
|
|
|
|
重新检测连接
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<!-- Step 3 summary -->
|
|
|
|
|
|
<div class="bg-slate-50 rounded-lg p-4 mb-4 text-sm">
|
|
|
|
|
|
<div class="font-semibold text-green-600 mb-2">✓ 步骤 3 配置完成</div>
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-1">
|
|
|
|
|
|
<span>数据库: <code class="bg-slate-200 px-1 rounded" x-text="form.db_name"></code></span>
|
|
|
|
|
|
<span>用户: <code class="bg-slate-200 px-1 rounded" x-text="form.db_user"></code></span>
|
|
|
|
|
|
<span>连接池: <code class="bg-slate-200 px-1 rounded" x-text="initResult?.pool_size || '—'"></code></span>
|
|
|
|
|
|
<span>溢出池: <code class="bg-slate-200 px-1 rounded" x-text="initResult?.max_overflow || '—'"></code></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form @submit.prevent="createAdmin()">
|
|
|
|
|
|
<!-- Brand -->
|
|
|
|
|
|
<div class="border-t-2 border-slate-100 pt-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-3">系统品牌</h3>
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">系统名称</label>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-1">显示在浏览器标题和后台标题栏</p>
|
|
|
|
|
|
<input x-model="adminForm.system_name" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">系统标题</label>
|
|
|
|
|
|
<input x-model="adminForm.system_title" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Admin account -->
|
|
|
|
|
|
<div class="border-t-2 border-slate-100 pt-4 mt-4">
|
|
|
|
|
|
<h3 class="font-bold text-slate-700 mb-3">管理员账号</h3>
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">管理员用户名</label>
|
|
|
|
|
|
<input x-model="adminForm.admin_username" required class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">管理员密码</label>
|
|
|
|
|
|
<p class="text-xs text-[var(--text-secondary)] mb-1">至少6位字符</p>
|
|
|
|
|
|
<input x-model="adminForm.admin_password" type="password" required minlength="6" placeholder="请输入安全密码"
|
|
|
|
|
|
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-3">
|
|
|
|
|
|
<label class="block font-semibold text-sm text-slate-700 mb-1">邮箱(可选)</label>
|
|
|
|
|
|
<input x-model="adminForm.admin_email" type="email" placeholder="admin@example.com"
|
|
|
|
|
|
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex gap-3 mt-5">
|
2026-06-06 04:32:47 +08:00
|
|
|
|
<button type="submit" :disabled="loading || !connAllPass" class="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-5 py-2.5 rounded-lg font-semibold transition flex items-center gap-2">
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<span x-show="loading" class="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full"></span>
|
2026-06-06 04:32:47 +08:00
|
|
|
|
<span x-text="loading ? '创建中...' : (connAllPass ? '创建账号 →' : '须先通过连接检测')"></span>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" @click="step = 3" class="bg-slate-200 hover:bg-slate-300 text-slate-700 px-5 py-2.5 rounded-lg font-semibold transition">← 上一步</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ======== Step 5: Complete ======== -->
|
|
|
|
|
|
<template x-if="step === 5 && !installed">
|
|
|
|
|
|
<div class="text-center">
|
|
|
|
|
|
<div class="w-16 h-16 rounded-full bg-green-100 text-green-500 text-3xl flex items-center justify-center mx-auto mb-4">✓</div>
|
|
|
|
|
|
<h2 class="text-xl font-bold text-slate-800 mb-2">安装完成!</h2>
|
|
|
|
|
|
<p class="text-[var(--text-muted)] mb-1">Nexus 6.0 已成功安装。</p>
|
|
|
|
|
|
<p class="text-[var(--text-secondary)] text-xs">安装向导已锁定 — 防止重复安装(如需重装请删除 .env 文件后重启服务)</p>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Guardian results -->
|
|
|
|
|
|
<div x-show="initResult?.guardian_results?.length" class="text-left mt-6 mb-4 rounded-lg p-4"
|
|
|
|
|
|
:class="guardianAllOk ? 'bg-green-50 border-l-4 border-green-500' : 'bg-amber-50 border-l-4 border-amber-500'">
|
|
|
|
|
|
<div class="font-semibold text-sm mb-2" :class="guardianAllOk ? 'text-green-700' : 'text-amber-700'"
|
|
|
|
|
|
x-text="guardianAllOk ? '✓ 进程守护已自动配置' : '⚠ 进程守护部分配置失败'"></div>
|
|
|
|
|
|
<template x-for="r in (initResult?.guardian_results || [])" :key="r">
|
|
|
|
|
|
<div class="text-xs leading-relaxed" x-text="r"></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Post-install checklist -->
|
|
|
|
|
|
<div class="text-left mt-6 space-y-3">
|
|
|
|
|
|
<h3 class="text-sm font-bold text-slate-700 text-center mb-3">━━━ 后续配置清单(必须完成)━━━</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 1. Supervisor -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">1</span>
|
|
|
|
|
|
Supervisor 进程守护
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!guardianAllOk" class="text-xs text-[var(--text-muted)] space-y-1">
|
|
|
|
|
|
<!-- BT Panel mode -->
|
|
|
|
|
|
<template x-if="btPanel">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p>宝塔面板 → 软件商店 → Supervisor管理器 → 添加守护进程</p>
|
|
|
|
|
|
<p>或手动写入配置: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">/www/server/panel/plugin/supervisor/profile/nexus.ini</code></p>
|
|
|
|
|
|
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'supervisorConf')">复制配置</button><pre id="supervisorConf">[program:nexus]
|
2026-05-25 08:23:37 +08:00
|
|
|
|
command=<span x-text="installDir"></span>/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port <span x-text="form.api_port"></span>
|
|
|
|
|
|
directory=<span x-text="installDir"></span>
|
|
|
|
|
|
user=root
|
|
|
|
|
|
autostart=true
|
|
|
|
|
|
autorestart=true
|
|
|
|
|
|
startretries=10
|
|
|
|
|
|
startsecs=3
|
|
|
|
|
|
stopwaitsecs=10
|
|
|
|
|
|
stopsignal=INT
|
|
|
|
|
|
environment=PATH="<span x-text="installDir"></span>/venv/bin:%(ENV_PATH)s"
|
|
|
|
|
|
stderr_logfile=/www/wwwlogs/nexus_error.log
|
|
|
|
|
|
stdout_logfile=/www/wwwlogs/nexus_access.log
|
|
|
|
|
|
stderr_logfile_maxbytes=50MB
|
|
|
|
|
|
stdout_logfile_maxbytes=50MB</pre></div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<p>重载: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">supervisorctl reread && supervisorctl update && supervisorctl start nexus</code></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- Standard mode -->
|
|
|
|
|
|
<template x-if="!btPanel">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p>安装: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo apt install -y supervisor</code></p>
|
|
|
|
|
|
<p>配置文件复制到: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">/etc/supervisor/conf.d/nexus.conf</code></p>
|
|
|
|
|
|
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'supervisorConf2')">复制配置</button><pre id="supervisorConf2">[program:nexus]
|
2026-05-25 08:23:37 +08:00
|
|
|
|
command=<span x-text="installDir"></span>/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port <span x-text="form.api_port"></span>
|
|
|
|
|
|
directory=<span x-text="installDir"></span>
|
|
|
|
|
|
user=root
|
|
|
|
|
|
autostart=true
|
|
|
|
|
|
autorestart=true
|
|
|
|
|
|
startretries=10
|
|
|
|
|
|
startsecs=3
|
|
|
|
|
|
stopwaitsecs=10
|
|
|
|
|
|
stopsignal=INT
|
|
|
|
|
|
environment=PATH="<span x-text="installDir"></span>/venv/bin:%(ENV_PATH)s"
|
|
|
|
|
|
stderr_logfile=/var/log/nexus/error.log
|
|
|
|
|
|
stdout_logfile=/var/log/nexus/access.log
|
|
|
|
|
|
stderr_logfile_maxbytes=50MB
|
|
|
|
|
|
stdout_logfile_maxbytes=50MB</pre></div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<p>启动: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start nexus</code></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="guardianAllOk" class="text-xs text-green-600">✓ 已自动配置 — Supervisor 配置已写入,服务已重载</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 2. Python venv -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">2</span>
|
|
|
|
|
|
Python 虚拟环境 + 依赖
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-[var(--text-muted)] space-y-1">
|
|
|
|
|
|
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">cd <span x-text="installDir"></span></code></p>
|
|
|
|
|
|
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">python3.12 -m venv venv</code></p>
|
|
|
|
|
|
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">source venv/bin/activate</code></p>
|
|
|
|
|
|
<p><code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">pip install -r requirements.txt</code></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 3. Nginx -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">3</span>
|
|
|
|
|
|
Nginx 反向代理 (API + WebSocket)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-[var(--text-muted)]">
|
|
|
|
|
|
<!-- BT Panel mode -->
|
|
|
|
|
|
<template x-if="btPanel">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="mb-1">宝塔 → 网站 → 配置文件 → <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">#PHP-INFO-END</code> 后面粘贴:</p>
|
|
|
|
|
|
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'nginxConf')">复制配置</button><pre id="nginxConf"> # Nexus Python API + WebSocket
|
|
|
|
|
|
location ^~ /api/ {
|
|
|
|
|
|
proxy_pass http://127.0.0.1:<span x-text="form.api_port"></span>;
|
|
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
|
|
}
|
|
|
|
|
|
location ^~ /health {
|
|
|
|
|
|
proxy_pass http://127.0.0.1:<span x-text="form.api_port"></span>;
|
|
|
|
|
|
}
|
|
|
|
|
|
location ^~ /ws/ {
|
|
|
|
|
|
proxy_pass http://127.0.0.1:<span x-text="form.api_port"></span>;
|
|
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
|
|
|
|
proxy_set_header Connection "upgrade";
|
|
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
|
}</pre></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- Standard mode -->
|
|
|
|
|
|
<template x-if="!btPanel">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="mb-1">将以下配置写入 <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">/etc/nginx/sites-available/nexus</code> 并创建符号链接到 sites-enabled:</p>
|
|
|
|
|
|
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'nginxConf2')">复制配置</button><pre id="nginxConf2">upstream nexus_api {
|
|
|
|
|
|
server 127.0.0.1:<span x-text="form.api_port"></span>;
|
|
|
|
|
|
keepalive 32;
|
2026-05-25 08:23:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
server {
|
2026-05-31 04:18:55 +08:00
|
|
|
|
listen 80;
|
|
|
|
|
|
server_name YOUR_DOMAIN;
|
|
|
|
|
|
|
|
|
|
|
|
location / {
|
|
|
|
|
|
proxy_pass http://nexus_api;
|
|
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
|
|
proxy_read_timeout 300s;
|
|
|
|
|
|
}
|
|
|
|
|
|
location /ws/ {
|
|
|
|
|
|
proxy_pass http://nexus_api;
|
|
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
|
|
|
|
proxy_set_header Connection "upgrade";
|
|
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
|
proxy_read_timeout 3600s;
|
|
|
|
|
|
}
|
|
|
|
|
|
location ~ ^/(\.env|\.git) {
|
|
|
|
|
|
return 404;
|
|
|
|
|
|
}
|
2026-05-25 08:23:37 +08:00
|
|
|
|
}</pre></div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<p>启用: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/ && sudo nginx -t && sudo systemctl reload nginx</code></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 4. Nginx Rewrite -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">4</span>
|
|
|
|
|
|
Nginx 伪静态
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-[var(--text-muted)]">
|
|
|
|
|
|
<p class="mb-1">宝塔 → 网站 → 伪静态 → 粘贴:</p>
|
|
|
|
|
|
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'nginxRewrite')">复制</button><pre id="nginxRewrite">location /app/ {
|
|
|
|
|
|
try_files $uri $uri/ /app/index.html;
|
2026-05-22 08:37:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
location / {
|
2026-05-31 04:18:55 +08:00
|
|
|
|
try_files $uri $uri/ =404;
|
2026-05-22 08:37:01 +08:00
|
|
|
|
}</pre></div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<!-- 5. SSL -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">5</span>
|
|
|
|
|
|
SSL证书 (强制HTTPS)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-[var(--text-muted)]">
|
|
|
|
|
|
<template x-if="btPanel">
|
|
|
|
|
|
<div>宝塔 → 网站 → SSL → Let's Encrypt → 一键申请 → 开启强制HTTPS</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template x-if="!btPanel">
|
|
|
|
|
|
<div>安装certbot: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo apt install -y certbot python3-certbot-nginx</code><br>
|
|
|
|
|
|
申请证书: <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">sudo certbot --nginx -d YOUR_DOMAIN</code></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 6. Health Monitor -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">6</span>
|
|
|
|
|
|
Shell 健康检查 (外部守护 + Telegram告警)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="!guardianAllOk" class="text-xs text-[var(--text-muted)]">
|
|
|
|
|
|
<p>配置 crontab:</p>
|
|
|
|
|
|
<div class="code-block"><button class="copy-btn" @click="copyCode($event, 'crontab')">复制</button><pre id="crontab">* * * * * <span x-text="installDir"></span>/deploy/health_monitor.sh</pre></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="guardianAllOk" class="text-xs text-green-600">✓ 已自动配置 — health_monitor.sh INSTALL_DIR 已更新,Crontab 已添加</div>
|
|
|
|
|
|
<div class="text-xs text-[var(--text-muted)] mt-2">
|
|
|
|
|
|
<b>3层守护机制:</b><br>
|
|
|
|
|
|
Layer 1: Supervisor — Python崩溃自动重启<br>
|
|
|
|
|
|
Layer 2: Python self_monitor — 每30s检查Redis/MySQL/WebSocket<br>
|
|
|
|
|
|
Layer 3: Shell脚本 — 每分钟HTTP检查,连续3次失败→Supervisor重启+Telegram告警
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
2026-05-31 04:18:55 +08:00
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
<!-- 7. Security -->
|
|
|
|
|
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
|
|
|
|
<div class="font-bold text-sm text-slate-800 mb-2">
|
|
|
|
|
|
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold mr-2">7</span>
|
|
|
|
|
|
安全收尾
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-[var(--text-muted)] space-y-1">
|
|
|
|
|
|
<p>✓ 安装向导已锁定 — 删除 <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">.env</code> 文件可重新安装</p>
|
|
|
|
|
|
<p>✓ <code class="bg-[var(--bg-surface)] text-slate-200 px-1.5 py-0.5 rounded">.env</code> 包含 SECRET_KEY/API_KEY — <b>安装后不可修改</b>(加密一致性)</p>
|
|
|
|
|
|
<p>✓ 防火墙限制端口 <span x-text="form.api_port"></span> (仅允许本机和子服务器IP)</p>
|
|
|
|
|
|
<p>✓ 修改管理员默认密码</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex justify-center mt-6">
|
|
|
|
|
|
<a href="/app/index.html" class="bg-green-500 hover:bg-green-600 text-white px-8 py-3 rounded-lg font-semibold text-lg transition">
|
|
|
|
|
|
进入管理面板 →
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
2026-05-22 08:37:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
function installWizard() {
|
2026-05-31 04:18:55 +08:00
|
|
|
|
const API = window.location.origin;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
step: 1,
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
installed: false,
|
|
|
|
|
|
error: '',
|
|
|
|
|
|
success: '',
|
|
|
|
|
|
envLoading: false,
|
|
|
|
|
|
envChecks: [],
|
|
|
|
|
|
envAllPass: false,
|
2026-06-06 04:32:47 +08:00
|
|
|
|
connLoading: false,
|
|
|
|
|
|
connChecks: [],
|
|
|
|
|
|
connAllPass: false,
|
2026-06-06 06:47:22 +08:00
|
|
|
|
credLoading: false,
|
|
|
|
|
|
credChecks: [],
|
|
|
|
|
|
credAllPass: false,
|
2026-05-31 04:18:55 +08:00
|
|
|
|
initResult: null,
|
2026-06-04 15:57:49 +08:00
|
|
|
|
installToken: '',
|
2026-05-31 04:18:55 +08:00
|
|
|
|
installDir: '/opt/nexus',
|
|
|
|
|
|
btPanel: false,
|
|
|
|
|
|
|
|
|
|
|
|
form: {
|
|
|
|
|
|
db_host: 'localhost',
|
|
|
|
|
|
db_port: '3306',
|
|
|
|
|
|
db_name: 'Nexus',
|
|
|
|
|
|
db_user: 'Nexus',
|
|
|
|
|
|
db_pass: '',
|
|
|
|
|
|
redis_host: '127.0.0.1',
|
|
|
|
|
|
redis_port: '6379',
|
|
|
|
|
|
redis_db: '0',
|
2026-06-06 06:59:45 +08:00
|
|
|
|
redis_user: '',
|
2026-05-31 04:18:55 +08:00
|
|
|
|
redis_pass: '',
|
|
|
|
|
|
api_port: '8600',
|
|
|
|
|
|
timezone: 'Asia/Shanghai',
|
|
|
|
|
|
site_url: '',
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
adminForm: {
|
|
|
|
|
|
admin_username: 'admin',
|
|
|
|
|
|
admin_password: '',
|
|
|
|
|
|
admin_email: '',
|
|
|
|
|
|
system_name: 'Nexus',
|
|
|
|
|
|
system_title: 'Nexus — 服务器运维管理平台',
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
get guardianAllOk() {
|
|
|
|
|
|
const results = this.initResult?.guardian_results || [];
|
|
|
|
|
|
if (!results.length) return false;
|
|
|
|
|
|
return results.every(r => !r.includes('✗') && !r.includes('⚠'));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-06 05:47:43 +08:00
|
|
|
|
isStaleDockerHost(host) {
|
|
|
|
|
|
const h = (host || '').trim().toLowerCase();
|
|
|
|
|
|
return !h || h === 'localhost' || h === '127.0.0.1' || h === 'host.docker.internal';
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-06 07:15:57 +08:00
|
|
|
|
isStaleRedisUser(user) {
|
|
|
|
|
|
const u = (user || '').trim().toLowerCase();
|
|
|
|
|
|
return u === 'root';
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-06 05:47:43 +08:00
|
|
|
|
applyDockerDefaults(defaults) {
|
|
|
|
|
|
if (!defaults) return;
|
|
|
|
|
|
if (this.isStaleDockerHost(this.form.db_host) && defaults.db_host) {
|
|
|
|
|
|
this.form.db_host = defaults.db_host;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.isStaleDockerHost(this.form.redis_host) && defaults.redis_host) {
|
|
|
|
|
|
this.form.redis_host = defaults.redis_host;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.form.db_name || this.form.db_name === 'Nexus') this.form.db_name = defaults.db_name || this.form.db_name;
|
|
|
|
|
|
if (!this.form.db_user || this.form.db_user === 'Nexus') this.form.db_user = defaults.db_user || this.form.db_user;
|
|
|
|
|
|
if (defaults.db_port) this.form.db_port = defaults.db_port;
|
|
|
|
|
|
if (defaults.redis_port) this.form.redis_port = defaults.redis_port;
|
|
|
|
|
|
if (defaults.redis_db) this.form.redis_db = defaults.redis_db;
|
2026-06-06 07:15:57 +08:00
|
|
|
|
if (this.isStaleRedisUser(this.form.redis_user)) this.form.redis_user = '';
|
2026-06-06 05:47:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async refreshDockerDefaults() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/env-check');
|
|
|
|
|
|
if (!r.ok) return;
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
this.applyDockerDefaults(d.docker_defaults);
|
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
async init() {
|
|
|
|
|
|
// Auto-detect site URL
|
|
|
|
|
|
const proto = location.protocol === 'https:' ? 'https' : 'http';
|
|
|
|
|
|
this.form.site_url = proto + '://' + location.host;
|
|
|
|
|
|
|
|
|
|
|
|
// Check install status
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/status');
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
this.installed = d.installed;
|
|
|
|
|
|
if (d.installed) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
|
|
|
|
|
|
|
// Try to restore state
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/state');
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
if (d.step > 1) this.step = d.step;
|
|
|
|
|
|
if (d.db_host) Object.assign(this.form, {
|
|
|
|
|
|
db_host: d.db_host, db_port: d.db_port, db_name: d.db_name,
|
|
|
|
|
|
db_user: d.db_user, db_pass: d.db_pass, api_port: d.api_port || '8600',
|
|
|
|
|
|
});
|
|
|
|
|
|
if (d.install_dir) this.installDir = d.install_dir;
|
|
|
|
|
|
if (d.pool_size) this.initResult = { pool_size: d.pool_size, max_overflow: d.max_overflow, guardian_results: d.guardian_results || [] };
|
2026-06-06 04:32:47 +08:00
|
|
|
|
if (d.step >= 4) this.checkConnections();
|
2026-05-31 04:18:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch(e) {}
|
2026-06-06 05:47:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 1Panel: override stale host.docker.internal with container names from API/volume
|
|
|
|
|
|
await this.refreshDockerDefaults();
|
2026-05-31 04:18:55 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-06 06:47:22 +08:00
|
|
|
|
invalidateCredCheck() {
|
|
|
|
|
|
this.credAllPass = false;
|
|
|
|
|
|
this.credChecks = [];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async testCredentials() {
|
|
|
|
|
|
this.credLoading = true;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
this.credAllPass = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/test-credentials', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify(this.form),
|
|
|
|
|
|
});
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
this.credChecks = d.checks || [];
|
|
|
|
|
|
this.credAllPass = !!d.all_pass;
|
|
|
|
|
|
if (!this.credAllPass) {
|
|
|
|
|
|
this.error = 'MySQL 或 Redis 连接未通过,请核对配置后重试。';
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.credChecks = d.checks || [];
|
|
|
|
|
|
this.error = d.detail || '连接检测失败';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.error = '连接检测请求失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.credLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-06 04:32:47 +08:00
|
|
|
|
async checkConnections() {
|
|
|
|
|
|
this.connLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/connection-check');
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
this.connChecks = d.checks || [];
|
|
|
|
|
|
this.connAllPass = !!d.all_pass;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const d = await r.json().catch(() => ({}));
|
|
|
|
|
|
this.connChecks = [{ name: '连接检测', pass: false, current: d.detail || '请先完成步骤3' }];
|
|
|
|
|
|
this.connAllPass = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.connChecks = [{ name: '连接检测', pass: false, current: '请求失败: ' + e.message }];
|
|
|
|
|
|
this.connAllPass = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.connLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-31 04:18:55 +08:00
|
|
|
|
async checkEnv() {
|
|
|
|
|
|
this.envLoading = true;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/env-check');
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
this.envChecks = d.checks;
|
|
|
|
|
|
this.envAllPass = d.all_pass;
|
2026-06-06 02:31:11 +08:00
|
|
|
|
if (d.docker_defaults) {
|
2026-06-06 05:47:43 +08:00
|
|
|
|
this.applyDockerDefaults(d.docker_defaults);
|
2026-06-06 02:31:11 +08:00
|
|
|
|
}
|
2026-05-31 04:18:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch(e) {
|
|
|
|
|
|
this.error = '环境检测请求失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.envLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async initDb() {
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
this.success = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(API + '/api/install/init-db', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify(this.form),
|
|
|
|
|
|
});
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
if (r.ok && d.success) {
|
|
|
|
|
|
this.success = '数据库初始化成功!已创建 ' + (d.tables_created || 14) + ' 张表。';
|
|
|
|
|
|
this.initResult = d;
|
2026-06-04 15:57:49 +08:00
|
|
|
|
this.installToken = d.install_token || '';
|
2026-05-31 04:18:55 +08:00
|
|
|
|
this.btPanel = !!d.bt_panel;
|
|
|
|
|
|
this.step = 4;
|
2026-06-06 04:32:47 +08:00
|
|
|
|
await this.checkConnections();
|
2026-05-31 04:18:55 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
this.error = d.detail || '初始化失败';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch(e) {
|
|
|
|
|
|
this.error = '请求失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async createAdmin() {
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const body = {
|
2026-06-04 15:57:49 +08:00
|
|
|
|
install_token: this.installToken,
|
2026-05-31 04:18:55 +08:00
|
|
|
|
...this.adminForm,
|
|
|
|
|
|
db_host: this.form.db_host,
|
|
|
|
|
|
db_port: this.form.db_port,
|
|
|
|
|
|
db_name: this.form.db_name,
|
|
|
|
|
|
db_user: this.form.db_user,
|
|
|
|
|
|
db_pass: this.form.db_pass,
|
|
|
|
|
|
};
|
|
|
|
|
|
const r = await fetch(API + '/api/install/create-admin', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
|
});
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
if (r.ok && d.success) {
|
2026-06-04 14:01:14 +08:00
|
|
|
|
if (d.locked) {
|
|
|
|
|
|
this.step = 5;
|
|
|
|
|
|
this.success = '安装完成!请使用刚创建的管理员账户登录。';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const lr = await fetch(API + '/api/install/lock', { method: 'POST' });
|
|
|
|
|
|
let ld = {};
|
|
|
|
|
|
try { ld = await lr.json(); } catch (_) {}
|
|
|
|
|
|
if (lr.ok || ld.already_locked) {
|
|
|
|
|
|
this.step = 5;
|
|
|
|
|
|
this.success = '安装完成!请使用刚创建的管理员账户登录。';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.error = (ld.detail || '管理员已创建,但锁定安装向导失败,请再次点击完成或刷新后重试。');
|
|
|
|
|
|
}
|
2026-05-31 04:18:55 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
this.error = d.detail || '创建管理员失败';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch(e) {
|
|
|
|
|
|
this.error = '请求失败: ' + e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
copyCode(event, id) {
|
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
const text = el.textContent;
|
|
|
|
|
|
const btn = event.target;
|
|
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
|
|
|
|
btn.textContent = '已复制 ✓';
|
|
|
|
|
|
btn.classList.add('copied');
|
|
|
|
|
|
setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('copied'); }, 2000);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-05-22 08:37:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|