293 lines
13 KiB
PHP
293 lines
13 KiB
PHP
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>@yield('title', 'AIWeb 管理后台')</title>
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<meta name="robots" content="noindex,nofollow">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta21/dist/css/tabler.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
|
<style>
|
|
:root { --brand: #3b82f6; }
|
|
body { font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f4f6fb; }
|
|
.brand-mark {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
|
color: #fff;
|
|
font-weight: 700;
|
|
letter-spacing: .4px;
|
|
}
|
|
.sidebar-caption {
|
|
font-size: .72rem;
|
|
letter-spacing: .06em;
|
|
text-transform: uppercase;
|
|
color: #94a3b8;
|
|
margin: .75rem 1rem .35rem;
|
|
}
|
|
.card-compact .card-body { padding: 1rem 1.1rem; }
|
|
.kpi-number { font-size: 1.75rem; font-weight: 700; }
|
|
.logout-btn {
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: .65rem;
|
|
background: #fff;
|
|
color: #334155;
|
|
font-size: .85rem;
|
|
padding: .38rem .7rem;
|
|
}
|
|
.md-editor-dropzone {
|
|
border: 1px dashed #cbd5e1;
|
|
border-radius: .6rem;
|
|
transition: .2s ease;
|
|
}
|
|
.md-editor-dropzone.dragover {
|
|
border-color: #3b82f6;
|
|
background: #eff6ff;
|
|
}
|
|
</style>
|
|
@yield('head')
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
|
<div class="container-fluid">
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="切换导航">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<h1 class="navbar-brand navbar-brand-autodark d-flex align-items-center gap-2">
|
|
<span class="brand-mark">AI</span>
|
|
<span>AIWeb Admin</span>
|
|
</h1>
|
|
<div class="navbar-nav flex-row d-lg-none">
|
|
<a class="nav-link px-0" href="{{ route('home') }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>
|
|
</div>
|
|
<div class="collapse navbar-collapse" id="sidebar-menu">
|
|
<ul class="navbar-nav pt-lg-3">
|
|
<li class="sidebar-caption">总览</li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.dashboard')) active @endif" href="{{ route('admin.dashboard') }}"><span class="nav-link-icon"><i class="bi bi-grid"></i></span><span class="nav-link-title">控制台</span></a></li>
|
|
<li class="sidebar-caption">内容管理</li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.tools.*')) active @endif" href="{{ route('admin.tools.index') }}"><span class="nav-link-icon"><i class="bi bi-box-seam"></i></span><span class="nav-link-title">AI 工具</span></a></li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.models.*')) active @endif" href="{{ route('admin.models.index') }}"><span class="nav-link-icon"><i class="bi bi-cpu"></i></span><span class="nav-link-title">AI 模型</span></a></li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.articles.*')) active @endif" href="{{ route('admin.articles.index') }}"><span class="nav-link-icon"><i class="bi bi-newspaper"></i></span><span class="nav-link-title">AI 资讯</span></a></li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.guides.*')) active @endif" href="{{ route('admin.guides.index') }}"><span class="nav-link-icon"><i class="bi bi-journal-code"></i></span><span class="nav-link-title">AI 教程</span></a></li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.categories.*')) active @endif" href="{{ route('admin.categories.index') }}"><span class="nav-link-icon"><i class="bi bi-diagram-3"></i></span><span class="nav-link-title">分类维护</span></a></li>
|
|
<li class="sidebar-caption">运营设置</li>
|
|
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.settings.*')) active @endif" href="{{ route('admin.settings.index') }}"><span class="nav-link-icon"><i class="bi bi-sliders"></i></span><span class="nav-link-title">首页配置</span></a></li>
|
|
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.feedback.*')) active @endif" href="{{ route('admin.feedback.index') }}"><span class="nav-link-icon"><i class="bi bi-chat-left-dots"></i></span><span class="nav-link-title">反馈管理</span></a></li>
|
|
<li class="sidebar-caption">站点</li>
|
|
<li class="nav-item"><a class="nav-link" href="{{ route('home') }}" target="_blank"><span class="nav-link-icon"><i class="bi bi-globe2"></i></span><span class="nav-link-title">访问前台</span></a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="page-wrapper">
|
|
<header class="navbar navbar-expand-md d-none d-lg-flex d-print-none">
|
|
<div class="container-xl">
|
|
<div class="navbar-nav flex-row order-md-last d-flex align-items-center gap-2">
|
|
<div class="text-muted small">当前账号:{{ session('admin_username', 'admin') }}</div>
|
|
<form method="post" action="{{ route('admin.logout') }}" class="mb-0">
|
|
@csrf
|
|
<button class="logout-btn" type="submit"><i class="bi bi-box-arrow-right me-1"></i>退出登录</button>
|
|
</form>
|
|
</div>
|
|
<div class="collapse navbar-collapse" id="navbar-menu">
|
|
<div>
|
|
<h2 class="page-title mb-0">@yield('title', '管理后台')</h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="page-body">
|
|
<div class="container-xl">
|
|
@if(session('status'))
|
|
<div class="alert alert-success" role="alert">
|
|
<i class="bi bi-check-circle me-1"></i>{{ session('status') }}
|
|
</div>
|
|
@endif
|
|
|
|
@if($errors->any())
|
|
<div class="alert alert-danger" role="alert">
|
|
<div class="fw-semibold mb-1">提交失败,请检查以下问题:</div>
|
|
<ul class="mb-0">
|
|
@foreach($errors->all() as $error)
|
|
<li>{{ $error }}</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
@endif
|
|
|
|
@yield('content')
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta21/dist/js/tabler.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
const uploadEndpoint = '{{ route('admin.uploads.markdown-image') }}';
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
|
|
|
const insertTextAtCursor = (textarea, text) => {
|
|
const start = textarea.selectionStart ?? textarea.value.length;
|
|
const end = textarea.selectionEnd ?? textarea.value.length;
|
|
textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(end);
|
|
const nextPos = start + text.length;
|
|
textarea.selectionStart = nextPos;
|
|
textarea.selectionEnd = nextPos;
|
|
textarea.focus();
|
|
};
|
|
|
|
const setButtonLoading = (button, loading) => {
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
if (loading) {
|
|
button.dataset.originHtml = button.innerHTML;
|
|
button.disabled = true;
|
|
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>上传中...';
|
|
|
|
return;
|
|
}
|
|
|
|
button.disabled = false;
|
|
if (button.dataset.originHtml) {
|
|
button.innerHTML = button.dataset.originHtml;
|
|
}
|
|
};
|
|
|
|
const uploadImage = async (file) => {
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
const response = await fetch(uploadEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('upload-failed');
|
|
}
|
|
|
|
const payload = await response.json();
|
|
if (!payload || payload.success !== true || !payload.url) {
|
|
throw new Error('upload-failed');
|
|
}
|
|
|
|
return payload;
|
|
};
|
|
|
|
const buildMarkdownImageText = (payload, fileName = 'image') => {
|
|
if (typeof payload.markdown === 'string' && payload.markdown.trim() !== '') {
|
|
return `\n${payload.markdown.trim()}\n`;
|
|
}
|
|
|
|
const alt = fileName.replace(/\.[^.]+$/, '');
|
|
return `\n\n`;
|
|
};
|
|
|
|
const attachUploadButton = (button) => {
|
|
button.addEventListener('click', function () {
|
|
const selector = button.getAttribute('data-target');
|
|
const textarea = selector ? document.querySelector(selector) : null;
|
|
if (!textarea) {
|
|
return;
|
|
}
|
|
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = 'image/*';
|
|
input.onchange = async () => {
|
|
const file = input.files?.[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
setButtonLoading(button, true);
|
|
try {
|
|
const payload = await uploadImage(file);
|
|
insertTextAtCursor(textarea, buildMarkdownImageText(payload, file.name || 'image'));
|
|
} catch (_) {
|
|
alert('图片上传失败,请稍后重试');
|
|
} finally {
|
|
setButtonLoading(button, false);
|
|
}
|
|
};
|
|
input.click();
|
|
});
|
|
};
|
|
|
|
const attachEditorDragPaste = (textarea) => {
|
|
textarea.classList.add('md-editor-dropzone');
|
|
|
|
textarea.addEventListener('dragover', (event) => {
|
|
event.preventDefault();
|
|
textarea.classList.add('dragover');
|
|
});
|
|
|
|
textarea.addEventListener('dragleave', () => {
|
|
textarea.classList.remove('dragover');
|
|
});
|
|
|
|
textarea.addEventListener('drop', async (event) => {
|
|
event.preventDefault();
|
|
textarea.classList.remove('dragover');
|
|
|
|
const files = Array.from(event.dataTransfer?.files || []).filter((file) => file.type.startsWith('image/'));
|
|
if (files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
for (const file of files) {
|
|
const payload = await uploadImage(file);
|
|
insertTextAtCursor(textarea, buildMarkdownImageText(payload, file.name || 'image'));
|
|
}
|
|
} catch (_) {
|
|
alert('拖拽上传失败,请稍后重试');
|
|
}
|
|
});
|
|
|
|
textarea.addEventListener('paste', async (event) => {
|
|
const files = Array.from(event.clipboardData?.files || []).filter((file) => file.type.startsWith('image/'));
|
|
if (files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
try {
|
|
for (const file of files) {
|
|
const payload = await uploadImage(file);
|
|
insertTextAtCursor(textarea, buildMarkdownImageText(payload, file.name || 'image'));
|
|
}
|
|
} catch (_) {
|
|
alert('粘贴上传失败,请稍后重试');
|
|
}
|
|
});
|
|
};
|
|
|
|
document.querySelectorAll('.js-md-upload-btn').forEach(attachUploadButton);
|
|
document.querySelectorAll('textarea.js-md-editor').forEach(attachEditorDragPaste);
|
|
}());
|
|
</script>
|
|
@yield('scripts')
|
|
</body>
|
|
</html>
|
|
|