864 lines
33 KiB
PHP
864 lines
33 KiB
PHP
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|||
|
|
<title>SQL 安装器 - Laravel</title>
|
|||
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|||
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--primary-color: #6366f1;
|
|||
|
|
--success-color: #10b981;
|
|||
|
|
--warning-color: #f59e0b;
|
|||
|
|
--danger-color: #ef4444;
|
|||
|
|
--dark-color: #1e293b;
|
|||
|
|
--light-color: #f8fafc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
background: linear-gradient(135deg, #f0f4f8, #e2e8f0);
|
|||
|
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|||
|
|
padding: 2rem 0;
|
|||
|
|
min-height: 100vh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.logo-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.logo-header i {
|
|||
|
|
font-size: 2.5rem;
|
|||
|
|
color: var(--primary-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card {
|
|||
|
|
border-radius: 16px;
|
|||
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
|||
|
|
border: none;
|
|||
|
|
overflow: hidden;
|
|||
|
|
margin-bottom: 2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-header {
|
|||
|
|
background: var(--primary-color);
|
|||
|
|
color: white;
|
|||
|
|
font-weight: 600;
|
|||
|
|
font-size: 1.25rem;
|
|||
|
|
padding: 1.25rem 1.5rem;
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-body {
|
|||
|
|
padding: 2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-container {
|
|||
|
|
position: relative;
|
|||
|
|
height: 2.5rem;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
background-color: #e2e8f0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-bar {
|
|||
|
|
height: 100%;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
background: linear-gradient(90deg, var(--primary-color), #4f46e5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-container {
|
|||
|
|
background-color: #1e293b;
|
|||
|
|
color: #cbd5e1;
|
|||
|
|
font-family: 'Fira Code', monospace;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
height: 320px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item {
|
|||
|
|
margin-bottom: 6px;
|
|||
|
|
border-left: 2px solid transparent;
|
|||
|
|
padding-left: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item.info {
|
|||
|
|
border-left-color: #60a5fa;
|
|||
|
|
color: #bfdbfe;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item.success {
|
|||
|
|
border-left-color: var(--success-color);
|
|||
|
|
color: #6ee7b7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item.warning {
|
|||
|
|
border-left-color: var(--warning-color);
|
|||
|
|
color: #fcd34d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item.error {
|
|||
|
|
border-left-color: var(--danger-color);
|
|||
|
|
color: #fca5a5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-action {
|
|||
|
|
padding: 0.75rem 1.5rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
border: none;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary {
|
|||
|
|
background: linear-gradient(135deg, var(--primary-color), #4f46e5);
|
|||
|
|
box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary:hover {
|
|||
|
|
background: linear-gradient(135deg, #4f46e5, #4338ca);
|
|||
|
|
box-shadow: 0 6px 8px rgba(99, 102, 241, 0.4);
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-indicator {
|
|||
|
|
width: 18px;
|
|||
|
|
height: 18px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
display: inline-block;
|
|||
|
|
margin-right: 10px;
|
|||
|
|
box-shadow: 0 0 8px currentColor;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-idle {
|
|||
|
|
background-color: #94a3b8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-running {
|
|||
|
|
background-color: var(--primary-color);
|
|||
|
|
animation: pulse 1.5s infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-success {
|
|||
|
|
background-color: var(--success-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-error {
|
|||
|
|
background-color: var(--danger-color);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-card {
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-card h5 {
|
|||
|
|
color: var(--primary-color);
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-card .info-item {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
padding-bottom: 0.75rem;
|
|||
|
|
border-bottom: 1px solid #f1f5f9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-card .info-item:last-child {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
padding-bottom: 0;
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0% { opacity: 1; }
|
|||
|
|
50% { opacity: 0.6; }
|
|||
|
|
100% { opacity: 1; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-title {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: var(--dark-color);
|
|||
|
|
margin-bottom: 1.2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.config-section {
|
|||
|
|
background: rgba(241, 245, 249, 0.5);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.step-number {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
width: 36px;
|
|||
|
|
height: 36px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background-color: var(--primary-color);
|
|||
|
|
color: white;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-right: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.highlight {
|
|||
|
|
background: rgba(99, 102, 241, 0.1);
|
|||
|
|
border-left: 4px solid var(--primary-color);
|
|||
|
|
padding: 0.5rem 1rem;
|
|||
|
|
border-radius: 0 8px 8px 0;
|
|||
|
|
margin: 1rem 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.floating-message {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 2rem;
|
|||
|
|
right: 2rem;
|
|||
|
|
z-index: 1000;
|
|||
|
|
padding: 1rem 1.5rem;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(20px);
|
|||
|
|
transition: all 0.4s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.floating-message.show {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.floating-success {
|
|||
|
|
background: var(--success-color);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.floating-error {
|
|||
|
|
background: var(--danger-color);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.permission-hint {
|
|||
|
|
background-color: #f8d7da;
|
|||
|
|
border-left: 4px solid #dc3545;
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
margin: 15px 0;
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.permission-hint-title {
|
|||
|
|
color: #dc3545;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.permission-solution {
|
|||
|
|
background-color: #d1ecf1;
|
|||
|
|
border-left: 4px solid #0dcaf0;
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
margin-top: 10px;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="logo-header">
|
|||
|
|
<i class="fas fa-database"></i>
|
|||
|
|
<h1>Laravel SQL 安装器</h1>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 权限问题提示 -->
|
|||
|
|
<div class="permission-hint" id="permissionHint">
|
|||
|
|
<div class="permission-hint-title">
|
|||
|
|
<i class="fas fa-exclamation-triangle me-2"></i>检测到权限问题
|
|||
|
|
</div>
|
|||
|
|
<p>您的系统可能遇到了文件权限问题,导致无法加载PHP扩展。</p>
|
|||
|
|
|
|||
|
|
<div class="permission-solution">
|
|||
|
|
<h6><i class="fas fa-wrench me-2"></i>解决方法:</h6>
|
|||
|
|
<ol>
|
|||
|
|
<li>检查Web服务器用户(如www-data)是否有权访问PHP扩展目录</li>
|
|||
|
|
<li>在服务器终端执行:<code>sudo chmod -R a+r /usr/local/lib/php/extensions/</code></li>
|
|||
|
|
<li>如果使用SELinux/AppArmor,调整安全策略允许访问</li>
|
|||
|
|
<li>确保PHP进程有权执行<code>artisan</code>命令</li>
|
|||
|
|
</ol>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="row">
|
|||
|
|
<!-- 左侧配置面板 -->
|
|||
|
|
<div class="col-lg-6">
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-header">
|
|||
|
|
<i class="fas fa-sliders-h me-2"></i>数据库配置
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<form id="installForm">
|
|||
|
|
<div class="config-section">
|
|||
|
|
<div class="section-title">
|
|||
|
|
<i class="fas fa-server"></i>
|
|||
|
|
<span>数据库连接</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label for="host" class="form-label">主机</label>
|
|||
|
|
<input type="text" class="form-control" id="host"
|
|||
|
|
placeholder="数据库主机" value="{{ env('DB_HOST', '127.0.0.1') }}">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label for="port" class="form-label">端口</label>
|
|||
|
|
<input type="number" class="form-control" id="port"
|
|||
|
|
placeholder="端口号" value="{{ env('DB_PORT', 3306) }}">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label for="database" class="form-label">数据库名</label>
|
|||
|
|
<input type="text" class="form-control" id="database"
|
|||
|
|
placeholder="数据库名称" value="{{ env('DB_DATABASE', 'nl_admin') }}">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col-md-6 mb-3">
|
|||
|
|
<label for="username" class="form-label">用户名</label>
|
|||
|
|
<input type="text" class="form-control" id="username"
|
|||
|
|
placeholder="用户名" value="{{ env('DB_USERNAME', 'root') }}">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col-md-6 mb-3">
|
|||
|
|
<label for="password" class="form-label">密码</label>
|
|||
|
|
<input type="password" class="form-control" id="password"
|
|||
|
|
placeholder="密码" value="{{ env('DB_PASSWORD', '') }}">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="d-grid mt-3">
|
|||
|
|
<button type="button" class="btn btn-primary" id="testConnectionBtn">
|
|||
|
|
<i class="fas fa-plug"></i> 测试连接
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="config-section">
|
|||
|
|
<div class="section-title">
|
|||
|
|
<i class="fas fa-file-code"></i>
|
|||
|
|
<span>SQL 文件设置</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label for="sqlPath" class="form-label">SQL 文件路径</label>
|
|||
|
|
<div class="input-group">
|
|||
|
|
<span class="input-group-text"><i class="fas fa-folder"></i></span>
|
|||
|
|
<input type="text" class="form-control" id="sqlPath"
|
|||
|
|
placeholder="输入SQL文件路径" value="public/nl_admin.sql">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-text mt-1">相对于应用根目录的文件路径</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-check form-switch mb-3">
|
|||
|
|
<input class="form-check-input" type="checkbox" id="forceInstall">
|
|||
|
|
<label class="form-check-label" for="forceInstall">
|
|||
|
|
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
|
|||
|
|
强制安装(覆盖现有数据)
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="form-check form-switch mb-3">
|
|||
|
|
<input class="form-check-input" type="checkbox" id="backupDb">
|
|||
|
|
<label class="form-check-label" for="backupDb">
|
|||
|
|
<i class="fas fa-file-archive text-info me-2"></i>
|
|||
|
|
安装前备份数据库
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="d-grid gap-2 mt-4">
|
|||
|
|
<button type="button" class="btn btn-primary" id="startInstallBtn">
|
|||
|
|
<i class="fas fa-play-circle"></i> 开始安装
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-card">
|
|||
|
|
<h5><i class="fas fa-info-circle text-primary me-2"></i>使用说明</h5>
|
|||
|
|
|
|||
|
|
<div class="highlight">
|
|||
|
|
<p>1. 配置数据库连接信息</p>
|
|||
|
|
<p>2. 设置SQL文件路径(默认为<code>public/nl_admin.sql</code>)</p>
|
|||
|
|
<p>3. 测试数据库连接是否成功</p>
|
|||
|
|
<p>4. 点击"开始安装"执行SQL文件</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span><i class="fas fa-exclamation-triangle text-warning me-2"></i>重要提示:</span>
|
|||
|
|
<span class="text-end">强制安装选项会覆盖现有数据,请谨慎使用</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧监控面板 -->
|
|||
|
|
<div class="col-lg-6">
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|||
|
|
<div>
|
|||
|
|
<i class="fas fa-chart-line me-2"></i>安装进度
|
|||
|
|
</div>
|
|||
|
|
<div id="statusDisplay">
|
|||
|
|
<div class="status-indicator status-idle"></div>
|
|||
|
|
<span id="statusText">准备就绪</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|||
|
|
<span id="progressPercentage">0%</span>
|
|||
|
|
<span id="fileSize" class="badge bg-light text-dark">
|
|||
|
|
<i class="fas fa-file-alt me-1"></i> 文件大小: 0 KB
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="progress-container">
|
|||
|
|
<div id="progressBar" class="progress-bar" style="width: 0%"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="log-container" id="logContainer">
|
|||
|
|
<div class="log-item info">== 系统日志 ==</div>
|
|||
|
|
<div class="log-item info">等待操作开始...</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="d-grid gap-2 mt-4">
|
|||
|
|
<button type="button" class="btn btn-outline-primary" id="downloadLogBtn" style="display: none;">
|
|||
|
|
<i class="fas fa-download me-2"></i>下载日志报告
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-header">
|
|||
|
|
<i class="fas fa-database me-2"></i>数据库信息
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div class="row">
|
|||
|
|
<div class="col-md-6">
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span>状态:</span>
|
|||
|
|
<span id="dbStatus" class="text-primary">未连接</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span>驱动:</span>
|
|||
|
|
<span id="dbDriver" class="text-info">{{ env('DB_CONNECTION', '未设置') }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span>字符集:</span>
|
|||
|
|
<span>utf8mb4</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="col-md-6">
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span>主机:</span>
|
|||
|
|
<span id="dbHost">{{ env('DB_HOST', '未设置') }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span>端口:</span>
|
|||
|
|
<span id="dbPort">{{ env('DB_PORT', '未设置') }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span>数据库名:</span>
|
|||
|
|
<span id="dbName">{{ env('DB_DATABASE', '未设置') }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 操作结果提示 -->
|
|||
|
|
<div id="floatingMessage" class="floating-message">
|
|||
|
|
<i class="fas fa-exclamation-circle me-2"></i> <span id="floatingText"></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|||
|
|
<script>
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
// 获取所有DOM元素引用
|
|||
|
|
const elements = {
|
|||
|
|
logContainer: document.getElementById('logContainer'),
|
|||
|
|
progressBar: document.getElementById('progressBar'),
|
|||
|
|
progressPercentage: document.getElementById('progressPercentage'),
|
|||
|
|
startInstallBtn: document.getElementById('startInstallBtn'),
|
|||
|
|
testConnectionBtn: document.getElementById('testConnectionBtn'),
|
|||
|
|
downloadLogBtn: document.getElementById('downloadLogBtn'),
|
|||
|
|
statusIndicator: document.querySelector('.status-indicator'),
|
|||
|
|
statusText: document.getElementById('statusText'),
|
|||
|
|
dbStatus: document.getElementById('dbStatus'),
|
|||
|
|
dbHost: document.getElementById('dbHost'),
|
|||
|
|
dbPort: document.getElementById('dbPort'),
|
|||
|
|
dbName: document.getElementById('dbName'),
|
|||
|
|
dbDriver: document.getElementById('dbDriver'),
|
|||
|
|
floatingMessage: document.getElementById('floatingMessage'),
|
|||
|
|
floatingText: document.getElementById('floatingText'),
|
|||
|
|
permissionHint: document.getElementById('permissionHint')
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let installLog = [];
|
|||
|
|
let currentProgress = 0;
|
|||
|
|
|
|||
|
|
// 添加日志函数
|
|||
|
|
function addLog(message, type = 'info') {
|
|||
|
|
const logItem = document.createElement('div');
|
|||
|
|
logItem.className = `log-item ${type}`;
|
|||
|
|
|
|||
|
|
const now = new Date();
|
|||
|
|
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
|
|||
|
|
|
|||
|
|
logItem.textContent = `[${time}] ${message}`;
|
|||
|
|
elements.logContainer.appendChild(logItem);
|
|||
|
|
|
|||
|
|
// 滚动到底部
|
|||
|
|
elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
|
|||
|
|
|
|||
|
|
// 保存日志
|
|||
|
|
installLog.push({
|
|||
|
|
time: new Date().toISOString(),
|
|||
|
|
message,
|
|||
|
|
type
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 检测是否是权限错误
|
|||
|
|
if (message.includes('Permission denied')) {
|
|||
|
|
elements.permissionHint.style.display = 'block';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测进度信息
|
|||
|
|
detectProgress(message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从日志消息中检测进度
|
|||
|
|
function detectProgress(message) {
|
|||
|
|
// 如果消息中包含进度百分比
|
|||
|
|
if (message.includes('%') && /[0-9]{1,3}%/.test(message)) {
|
|||
|
|
const match = message.match(/(\d{1,3})%/);
|
|||
|
|
if (match && match[1]) {
|
|||
|
|
currentProgress = parseInt(match[1]);
|
|||
|
|
updateProgress(currentProgress);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 检测文件大小信息
|
|||
|
|
else if (message.includes('文件大小')) {
|
|||
|
|
const match = message.match(/大小: ([0-9.]+)MB/);
|
|||
|
|
if (match && match[1]) {
|
|||
|
|
const size = Math.round(parseFloat(match[1]) * 1024);
|
|||
|
|
elements.fileSize.innerHTML = `<i class="fas fa-file-alt me-1"></i> 文件大小: ${size} KB`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新状态显示
|
|||
|
|
function updateStatus(status) {
|
|||
|
|
elements.statusIndicator.className = 'status-indicator';
|
|||
|
|
|
|||
|
|
switch(status) {
|
|||
|
|
case 'idle':
|
|||
|
|
elements.statusIndicator.classList.add('status-idle');
|
|||
|
|
elements.statusText.textContent = '准备就绪';
|
|||
|
|
break;
|
|||
|
|
case 'running':
|
|||
|
|
elements.statusIndicator.classList.add('status-running');
|
|||
|
|
elements.statusText.textContent = '安装进行中...';
|
|||
|
|
elements.startInstallBtn.disabled = true;
|
|||
|
|
elements.testConnectionBtn.disabled = true;
|
|||
|
|
break;
|
|||
|
|
case 'success':
|
|||
|
|
elements.statusIndicator.classList.add('status-success');
|
|||
|
|
elements.statusText.textContent = '安装成功';
|
|||
|
|
elements.startInstallBtn.disabled = false;
|
|||
|
|
elements.testConnectionBtn.disabled = false;
|
|||
|
|
break;
|
|||
|
|
case 'error':
|
|||
|
|
elements.statusIndicator.classList.add('status-error');
|
|||
|
|
elements.statusText.textContent = '安装失败';
|
|||
|
|
elements.startInstallBtn.disabled = false;
|
|||
|
|
elements.testConnectionBtn.disabled = false;
|
|||
|
|
elements.downloadLogBtn.style.display = 'block';
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新数据库连接状态
|
|||
|
|
function updateDbStatus(status) {
|
|||
|
|
elements.dbStatus.textContent = status;
|
|||
|
|
elements.dbStatus.className = '';
|
|||
|
|
|
|||
|
|
switch(status) {
|
|||
|
|
case '已连接':
|
|||
|
|
elements.dbStatus.classList.add('text-success');
|
|||
|
|
break;
|
|||
|
|
case '连接失败':
|
|||
|
|
elements.dbStatus.classList.add('text-danger');
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
elements.dbStatus.classList.add('text-primary');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新进度条
|
|||
|
|
function updateProgress(percentage, message) {
|
|||
|
|
const percent = Math.min(100, Math.max(0, percentage));
|
|||
|
|
elements.progressBar.style.width = `${percent}%`;
|
|||
|
|
elements.progressPercentage.textContent = `${Math.round(percent)}%`;
|
|||
|
|
|
|||
|
|
if (message) {
|
|||
|
|
addLog(message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示浮动消息
|
|||
|
|
function showFloatingMessage(message, type) {
|
|||
|
|
elements.floatingText.textContent = message;
|
|||
|
|
|
|||
|
|
elements.floatingMessage.className = 'floating-message';
|
|||
|
|
elements.floatingMessage.classList.add('show');
|
|||
|
|
|
|||
|
|
if (type === 'success') {
|
|||
|
|
elements.floatingMessage.classList.add('floating-success');
|
|||
|
|
} else {
|
|||
|
|
elements.floatingMessage.classList.add('floating-error');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
elements.floatingMessage.classList.remove('show');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
elements.floatingMessage.className = 'floating-message';
|
|||
|
|
}, 300);
|
|||
|
|
}, 5000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 测试数据库连接
|
|||
|
|
async function testDatabaseConnection() {
|
|||
|
|
const host = document.getElementById('host').value;
|
|||
|
|
const port = document.getElementById('port').value;
|
|||
|
|
const database = document.getElementById('database').value;
|
|||
|
|
const username = document.getElementById('username').value;
|
|||
|
|
const password = document.getElementById('password').value;
|
|||
|
|
|
|||
|
|
// 更新数据库信息显示
|
|||
|
|
elements.dbHost.textContent = host || '未设置';
|
|||
|
|
elements.dbPort.textContent = port || '未设置';
|
|||
|
|
elements.dbName.textContent = database || '未设置';
|
|||
|
|
|
|||
|
|
addLog('正在测试数据库连接...', 'info');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch('/api/sql/test-connection', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
host,
|
|||
|
|
port,
|
|||
|
|
database,
|
|||
|
|
username,
|
|||
|
|
password
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await response.json();
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
addLog('✅ 数据库连接成功!', 'success');
|
|||
|
|
updateDbStatus('已连接');
|
|||
|
|
showFloatingMessage('✅ 数据库连接成功!', 'success');
|
|||
|
|
|
|||
|
|
// 更新数据库信息
|
|||
|
|
elements.dbDriver.textContent = 'MySQL';
|
|||
|
|
} else {
|
|||
|
|
const errorMessage = `❌ ${result.message || '数据库连接失败'}`;
|
|||
|
|
addLog(errorMessage, 'error');
|
|||
|
|
updateDbStatus('连接失败');
|
|||
|
|
showFloatingMessage(errorMessage, 'error');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
const errorMessage = `❌ 连接测试出错: ${error.message}`;
|
|||
|
|
addLog(errorMessage, 'error');
|
|||
|
|
updateDbStatus('连接失败');
|
|||
|
|
showFloatingMessage(errorMessage, 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 开始安装过程
|
|||
|
|
async function startInstallation() {
|
|||
|
|
// 重置日志和UI状态
|
|||
|
|
elements.logContainer.innerHTML = '<div class="log-item info">== 安装日志 ==</div>';
|
|||
|
|
installLog = [];
|
|||
|
|
currentProgress = 0;
|
|||
|
|
updateProgress(0);
|
|||
|
|
updateStatus('running');
|
|||
|
|
elements.downloadLogBtn.style.display = 'none';
|
|||
|
|
elements.permissionHint.style.display = 'none';
|
|||
|
|
|
|||
|
|
const sqlPath = document.getElementById('sqlPath').value;
|
|||
|
|
const host = document.getElementById('host').value;
|
|||
|
|
const port = document.getElementById('port').value;
|
|||
|
|
const database = document.getElementById('database').value;
|
|||
|
|
const username = document.getElementById('username').value;
|
|||
|
|
const password = document.getElementById('password').value;
|
|||
|
|
const forceInstall = document.getElementById('forceInstall').checked;
|
|||
|
|
const backupDb = document.getElementById('backupDb').checked;
|
|||
|
|
|
|||
|
|
addLog('🚀 开始数据库安装流程...', 'info');
|
|||
|
|
addLog(`📁 文件路径: ${sqlPath}`, 'info');
|
|||
|
|
addLog(`🔧 数据库配置: ${username}@${host}:${port}`, 'info');
|
|||
|
|
addLog(`🗃️ 数据库名: ${database}`, 'info');
|
|||
|
|
addLog(`⚡ 强制模式: ${forceInstall ? '是' : '否'}`, forceInstall ? 'warning' : 'info');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch('/api/sql/start-installation', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
file: sqlPath,
|
|||
|
|
host,
|
|||
|
|
port,
|
|||
|
|
database,
|
|||
|
|
username,
|
|||
|
|
password,
|
|||
|
|
force: forceInstall,
|
|||
|
|
backup: backupDb
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const result = await response.json();
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
// 处理错误情况
|
|||
|
|
const errorMsg = result.message || '安装过程中发生错误';
|
|||
|
|
|
|||
|
|
// 确保日志存在
|
|||
|
|
if (result.logs && Array.isArray(result.logs)) {
|
|||
|
|
result.logs.forEach(log => {
|
|||
|
|
let type = 'info';
|
|||
|
|
if (log.includes('❌') || log.includes('失败')) type = 'error';
|
|||
|
|
if (log.includes('⚠️')) type = 'warning';
|
|||
|
|
if (log.includes('✅')) type = 'success';
|
|||
|
|
|
|||
|
|
addLog(log, type);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
addLog(`❌ ${errorMsg}`, 'error');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showFloatingMessage(`❌ ${errorMsg}`, 'error');
|
|||
|
|
updateStatus('error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理成功情况
|
|||
|
|
if (result.success && Array.isArray(result.logs)) {
|
|||
|
|
result.logs.forEach(log => {
|
|||
|
|
let type = 'info';
|
|||
|
|
if (log.includes('❌') || log.includes('失败')) type = 'error';
|
|||
|
|
if (log.includes('⚠️')) type = 'warning';
|
|||
|
|
if (log.includes('✅')) type = 'success';
|
|||
|
|
|
|||
|
|
addLog(log, type);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 更新文件大小显示
|
|||
|
|
if (result.file_size) {
|
|||
|
|
const sizeKB = Math.round(result.file_size / 1024);
|
|||
|
|
elements.fileSize.innerHTML =
|
|||
|
|
`<i class="fas fa-file-alt me-1"></i> 文件大小: ${sizeKB} KB`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新进度到100%
|
|||
|
|
updateProgress(100, '✅ 安装完成');
|
|||
|
|
|
|||
|
|
showFloatingMessage('✅ 安装成功!', 'success');
|
|||
|
|
updateStatus('success');
|
|||
|
|
} else {
|
|||
|
|
addLog('❌ 安装完成但返回数据异常', 'error');
|
|||
|
|
showFloatingMessage('❌ 安装完成但返回数据异常', 'error');
|
|||
|
|
updateStatus('error');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
addLog(`❌ 安装过程中出错: ${error.message}`, 'error');
|
|||
|
|
showFloatingMessage(`❌ 安装失败: ${error.message}`, 'error');
|
|||
|
|
updateStatus('error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 下载日志报告
|
|||
|
|
function downloadLogReport() {
|
|||
|
|
const logContent = installLog.map(entry =>
|
|||
|
|
`[${new Date(entry.time).toLocaleTimeString()}] ${entry.message}`
|
|||
|
|
).join('\n');
|
|||
|
|
|
|||
|
|
const blob = new Blob([logContent], {type: 'text/plain'});
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
|
|||
|
|
const a = document.createElement('a');
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = `sql_install_log_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.log`;
|
|||
|
|
document.body.appendChild(a);
|
|||
|
|
a.click();
|
|||
|
|
document.body.removeChild(a);
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绑定事件监听器
|
|||
|
|
elements.startInstallBtn.addEventListener('click', startInstallation);
|
|||
|
|
elements.testConnectionBtn.addEventListener('click', testDatabaseConnection);
|
|||
|
|
elements.downloadLogBtn.addEventListener('click', downloadLogReport);
|
|||
|
|
|
|||
|
|
// 页面加载完成状态
|
|||
|
|
addLog('系统初始化完毕', 'info');
|
|||
|
|
updateStatus('idle');
|
|||
|
|
updateDbStatus('未连接');
|
|||
|
|
|
|||
|
|
// 初始显示数据库信息
|
|||
|
|
elements.dbHost.textContent = "{{ env('DB_HOST', '未设置') }}";
|
|||
|
|
elements.dbPort.textContent = "{{ env('DB_PORT', '未设置') }}";
|
|||
|
|
elements.dbName.textContent = "{{ env('DB_DATABASE', '未设置') }}";
|
|||
|
|
elements.dbDriver.textContent = "{{ env('DB_CONNECTION', '未设置') }}";
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|