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>
|