Files
nl-jqrl-api/resources/views/init.blade.php

864 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>