Files

864 lines
33 KiB
PHP
Raw Permalink Normal View History

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