feat: 初始化知识库系统项目

- 实现基于 Laravel 11 和 Filament 3.X 的文档管理系统
- 添加用户认证和分组管理功能
- 实现文档上传、分类和权限控制
- 集成 Word 文档自动转换为 Markdown
- 集成 Meilisearch 全文搜索引擎
- 实现文档在线预览功能
- 添加安全日志和审计功能
- 完整的简体中文界面
- 包含完整的项目文档和部署指南

技术栈:
- Laravel 11.x
- Filament 3.X
- Meilisearch 1.5+
- Pandoc 文档转换
- Redis 队列系统
- Pest PHP 测试框架
This commit is contained in:
Knowledge Base System
2025-12-05 14:44:44 +08:00
commit acf549c43c
165 changed files with 32838 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ $document->title }} - Markdown 预览</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f3f4f6;
}
.preview-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.preview-header {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.preview-content {
background: white;
border-radius: 8px;
padding: 40px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
min-height: 400px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6b7280;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 20px;
}
@media print {
.preview-header {
display: none;
}
.preview-content {
box-shadow: none;
padding: 0;
}
}
/* 移动端优化 */
@media (max-width: 768px) {
.preview-container {
padding: 10px;
}
.preview-header {
padding: 15px;
}
.preview-content {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="preview-container">
<!-- 头部信息 -->
<div class="preview-header">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex-1">
<h1 class="text-2xl font-bold text-gray-900 mb-2">{{ $document->title }}</h1>
<div class="flex flex-wrap gap-4 text-sm text-gray-600">
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
</svg>
<span>{{ $document->type === 'global' ? '全局知识库' : '专用知识库' }}</span>
</div>
@if($document->group)
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
<span>{{ $document->group->name }}</span>
</div>
@endif
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
<span>{{ $document->uploader->name }}</span>
</div>
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span>{{ $document->created_at->format('Y年m月d日 H:i') }}</span>
</div>
</div>
@if($document->description)
<p class="mt-3 text-gray-700">{{ $document->description }}</p>
@endif
</div>
<div class="flex gap-2">
<a href="{{ route('documents.download', $document) }}"
class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
<span>下载原文档</span>
</a>
<button onclick="window.print()"
class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
</svg>
<span>打印</span>
</button>
</div>
</div>
</div>
<!-- Markdown 内容 -->
<div class="preview-content">
@if($markdownHtml)
{!! $markdownHtml !!}
@else
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<h2 class="text-xl font-semibold text-gray-700 mb-2">Markdown 内容为空</h2>
<p class="text-gray-600 mb-6">该文档的 Markdown 内容尚未生成或为空</p>
<a href="{{ route('documents.download', $document) }}"
class="inline-flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
<span>下载原始文档</span>
</a>
</div>
@endif
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,101 @@
@php
use App\Services\DocumentPreviewService;
$previewService = app(DocumentPreviewService::class);
$htmlContent = null;
$error = null;
try {
$htmlContent = $previewService->convertToHtml($document);
} catch (\Exception $e) {
$error = $e->getMessage();
}
@endphp
<div class="document-preview-modal">
@if ($error)
<div class="rounded-lg bg-danger-50 p-4 text-danger-600 dark:bg-danger-400/10 dark:text-danger-400">
<div class="flex items-center gap-3">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<div>
<p class="font-semibold">预览失败</p>
<p class="text-sm">{{ $error }}</p>
</div>
</div>
</div>
@elseif ($htmlContent)
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div class="border-b border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900">
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
文档内容预览
</h3>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ $document->file_name }}
</span>
</div>
</div>
<div class="max-h-[600px] overflow-y-auto p-6">
<div class="prose prose-sm max-w-none dark:prose-invert">
{!! $htmlContent !!}
</div>
</div>
<div class="border-t border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900">
<p class="text-xs text-gray-500 dark:text-gray-400">
提示:这是文档的预览版本,可能与原始格式略有差异。如需查看完整格式,请下载文档。
</p>
</div>
</div>
@else
<div class="rounded-lg bg-gray-50 p-4 text-gray-600 dark:bg-gray-800 dark:text-gray-400">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p>正在加载文档预览...</p>
</div>
</div>
@endif
</div>
<style>
.document-preview-modal .prose {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.document-preview-modal .prose table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
.document-preview-modal .prose table td,
.document-preview-modal .prose table th {
border: 1px solid #e5e7eb;
padding: 0.5em;
}
.document-preview-modal .prose table th {
background-color: #f9fafb;
font-weight: 600;
}
.document-preview-modal .prose img {
max-width: 100%;
height: auto;
}
.dark .document-preview-modal .prose table td,
.dark .document-preview-modal .prose table th {
border-color: #374151;
}
.dark .document-preview-modal .prose table th {
background-color: #1f2937;
}
</style>

View File

@@ -0,0 +1,38 @@
<x-filament-panels::page>
<div class="space-y-6">
{{-- 搜索表单 --}}
<x-filament::card>
<form wire:submit.prevent="search">
{{ $this->form }}
<div class="mt-6 flex gap-3">
<x-filament::button type="submit" icon="heroicon-o-magnifying-glass">
搜索
</x-filament::button>
<x-filament::button type="button" color="gray" wire:click="clearSearch">
清空
</x-filament::button>
</div>
</form>
</x-filament::card>
{{-- 搜索结果 --}}
@if($hasSearched)
<x-filament::card>
<div class="space-y-4">
<h3 class="text-lg font-semibold">
搜索结果
@if($searchQuery)
<span class="text-sm font-normal text-gray-500">
- 关键词: "{{ $searchQuery }}"
</span>
@endif
</h3>
{{ $this->table }}
</div>
</x-filament::card>
@endif
</div>
</x-filament-panels::page>

View File

@@ -0,0 +1,116 @@
@php
use App\Services\DocumentPreviewService;
$previewService = app(DocumentPreviewService::class);
$canPreview = $previewService->canPreview($document);
$htmlContent = null;
$error = null;
if ($canPreview) {
try {
$htmlContent = $previewService->convertToHtml($document);
} catch (\Exception $e) {
$error = $e->getMessage();
}
}
@endphp
<div class="document-preview-container">
@if ($error)
<div class="rounded-lg bg-danger-50 p-4 text-danger-600 dark:bg-danger-400/10 dark:text-danger-400">
<div class="flex items-center gap-3">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<div>
<p class="font-semibold">预览失败</p>
<p class="text-sm">{{ $error }}</p>
</div>
</div>
</div>
@elseif (!$canPreview)
<div class="rounded-lg bg-warning-50 p-4 text-warning-600 dark:bg-warning-400/10 dark:text-warning-400">
<div class="flex items-center gap-3">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
<div>
<p class="font-semibold">无法预览此文档</p>
<p class="text-sm">该文档格式不支持在线预览,请下载后查看。</p>
</div>
</div>
</div>
@elseif ($htmlContent)
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div class="border-b border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900">
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
文档内容预览
</h3>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ $document->file_name }}
</span>
</div>
</div>
<div class="max-h-[600px] overflow-y-auto p-6">
<div class="prose prose-sm max-w-none dark:prose-invert">
{!! $htmlContent !!}
</div>
</div>
<div class="border-t border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900">
<p class="text-xs text-gray-500 dark:text-gray-400">
提示:这是文档的预览版本,可能与原始格式略有差异。如需查看完整格式,请下载文档。
</p>
</div>
</div>
@else
<div class="rounded-lg bg-gray-50 p-4 text-gray-600 dark:bg-gray-800 dark:text-gray-400">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p>正在加载文档预览...</p>
</div>
</div>
@endif
</div>
<style>
.document-preview-container .prose {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.document-preview-container .prose table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
.document-preview-container .prose table td,
.document-preview-container .prose table th {
border: 1px solid #e5e7eb;
padding: 0.5em;
}
.document-preview-container .prose table th {
background-color: #f9fafb;
font-weight: 600;
}
.document-preview-container .prose img {
max-width: 100%;
height: auto;
}
.dark .document-preview-container .prose table td,
.dark .document-preview-container .prose table th {
border-color: #374151;
}
.dark .document-preview-container .prose table th {
background-color: #1f2937;
}
</style>

File diff suppressed because one or more lines are too long