Files
ai-web/resources/views/layouts/admin.blade.php

333 lines
15 KiB
PHP
Raw Normal View History

2026-02-12 13:06:12 +08:00
<!doctype html>
2026-02-11 17:28:36 +08:00
<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">
2026-02-13 10:56:23 +08:00
<link href="{{ asset('vendor/tabler/css/tabler.min.css') }}" rel="stylesheet">
<link href="{{ asset('vendor/bootstrap-icons/font/bootstrap-icons.min.css') }}" rel="stylesheet">
2026-02-11 17:28:36 +08:00
<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;
}
2026-02-12 17:10:36 +08:00
.navbar-vertical .nav-link.active {
background: rgba(59, 130, 246, .2);
color: #fff;
border-radius: .6rem;
}
.admin-page-head {
padding: .15rem 0 .95rem;
margin-bottom: .45rem;
border-bottom: 1px solid #e5eaf4;
}
.admin-page-subtitle {
color: #64748b;
font-size: .88rem;
line-height: 1.55;
}
.admin-page-actions .btn {
border-radius: .6rem;
}
.admin-breadcrumb .breadcrumb {
--tblr-breadcrumb-divider-color: #94a3b8;
--tblr-breadcrumb-item-active-color: #475569;
font-size: .8rem;
}
.admin-breadcrumb .breadcrumb-item a {
color: #64748b;
text-decoration: none;
}
.admin-breadcrumb .breadcrumb-item a:hover {
color: #2563eb;
}
2026-02-11 17:28:36 +08:00
</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>
2026-02-12 17:10:36 +08:00
<li class="nav-item"><a class="nav-link @if(request()->routeIs('admin.sources.*')) active @endif" href="{{ route('admin.sources.index') }}"><span class="nav-link-icon"><i class="bi bi-shield-check"></i></span><span class="nav-link-title">来源管理</span></a></li>
2026-02-12 13:06:12 +08:00
<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>
2026-02-11 17:28:36 +08:00
<li class="sidebar-caption">站点</li>
2026-02-12 13:06:12 +08:00
<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>
2026-02-11 17:28:36 +08:00
</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
2026-02-12 13:06:12 +08:00
<button class="logout-btn" type="submit"><i class="bi bi-box-arrow-right me-1"></i>退出登录</button>
2026-02-11 17:28:36 +08:00
</form>
</div>
<div class="collapse navbar-collapse" id="navbar-menu">
<div>
2026-02-12 17:10:36 +08:00
@include('admin.partials.admin-page-header')
2026-02-11 17:28:36 +08:00
</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>
2026-02-13 10:56:23 +08:00
<script src="{{ asset('vendor/tabler/js/tabler.min.js') }}"></script>
2026-02-11 17:28:36 +08:00
<script>
(function () {
const uploadEndpoint = '{{ route('admin.uploads.markdown-image') }}';
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
2026-02-12 15:37:49 +08:00
const insertTextAtCursor = (field, text) => {
if (!(field instanceof HTMLInputElement) && !(field instanceof HTMLTextAreaElement)) {
return;
}
const start = field.selectionStart ?? field.value.length;
const end = field.selectionEnd ?? field.value.length;
field.value = field.value.substring(0, start) + text + field.value.substring(end);
2026-02-11 17:28:36 +08:00
const nextPos = start + text.length;
2026-02-12 15:37:49 +08:00
if (typeof field.selectionStart === 'number' && typeof field.selectionEnd === 'number') {
field.selectionStart = nextPos;
field.selectionEnd = nextPos;
}
field.focus();
2026-02-11 17:28:36 +08:00
};
const setButtonLoading = (button, loading) => {
if (!button) {
return;
}
if (loading) {
button.dataset.originHtml = button.innerHTML;
button.disabled = true;
2026-02-12 13:06:12 +08:00
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>上传中...';
2026-02-11 17:28:36 +08:00
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![${alt}](${payload.url})\n`;
};
const attachUploadButton = (button) => {
button.addEventListener('click', function () {
const selector = button.getAttribute('data-target');
2026-02-12 15:37:49 +08:00
const targetField = selector ? document.querySelector(selector) : null;
if (!(targetField instanceof HTMLInputElement) && !(targetField instanceof HTMLTextAreaElement)) {
2026-02-11 17:28:36 +08:00
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);
2026-02-12 15:37:49 +08:00
const imageText = targetField instanceof HTMLTextAreaElement
? buildMarkdownImageText(payload, file.name || 'image')
: payload.url;
insertTextAtCursor(targetField, imageText);
2026-02-11 17:28:36 +08:00
} 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>
2026-02-12 13:06:12 +08:00