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:
281
app/Filament/Pages/SearchPage.php
Normal file
281
app/Filament/Pages/SearchPage.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Document;
|
||||
use App\Models\Group;
|
||||
use App\Services\DocumentSearchService;
|
||||
use App\Services\DocumentService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SearchPage extends Page implements HasForms, HasTable
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-magnifying-glass';
|
||||
|
||||
protected static string $view = 'filament.pages.search-page';
|
||||
|
||||
protected static ?string $navigationLabel = '搜索文档';
|
||||
|
||||
protected static ?string $title = '搜索文档';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
// 表单数据
|
||||
public ?string $searchQuery = null;
|
||||
public ?string $documentType = null;
|
||||
public ?int $groupId = null;
|
||||
|
||||
// 搜索结果
|
||||
public $searchResults = null;
|
||||
public bool $hasSearched = false;
|
||||
|
||||
/**
|
||||
* 挂载页面时的初始化
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'searchQuery' => '',
|
||||
'documentType' => null,
|
||||
'groupId' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义搜索表单
|
||||
*/
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('searchQuery')
|
||||
->label('搜索关键词')
|
||||
->placeholder('请输入搜索关键词...')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Select::make('documentType')
|
||||
->label('文档类型')
|
||||
->placeholder('全部类型')
|
||||
->options([
|
||||
'global' => '全局知识库',
|
||||
'dedicated' => '专用知识库',
|
||||
])
|
||||
->native(false),
|
||||
|
||||
Select::make('groupId')
|
||||
->label('所属分组')
|
||||
->placeholder('全部分组')
|
||||
->options(Group::pluck('name', 'id'))
|
||||
->searchable()
|
||||
->native(false),
|
||||
])
|
||||
->columns(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义搜索结果表格
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query($this->getTableQuery())
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label('文档标题')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('markdown_preview')
|
||||
->label('内容片段')
|
||||
->limit(100)
|
||||
->wrap()
|
||||
->default('暂无内容预览'),
|
||||
|
||||
TextColumn::make('type')
|
||||
->label('文档类型')
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'global' => '全局知识库',
|
||||
'dedicated' => '专用知识库',
|
||||
default => $state,
|
||||
})
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'global' => 'success',
|
||||
'dedicated' => 'info',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
TextColumn::make('group.name')
|
||||
->label('所属分组')
|
||||
->default('无')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label('上传时间')
|
||||
->dateTime('Y-m-d H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
Action::make('preview')
|
||||
->label('预览')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('info')
|
||||
->modalHeading(fn (Document $record) => $record->title)
|
||||
->modalContent(fn (Document $record) => view('filament.pages.document-preview-modal', [
|
||||
'document' => $record,
|
||||
]))
|
||||
->modalWidth('7xl')
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('关闭')
|
||||
->visible(fn (Document $record) => $record->conversion_status === 'completed'),
|
||||
|
||||
Action::make('download')
|
||||
->label('下载')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->action(function (Document $record) {
|
||||
try {
|
||||
$documentService = app(DocumentService::class);
|
||||
$user = Auth::user();
|
||||
|
||||
// 记录下载日志
|
||||
$documentService->logDownload($record, $user);
|
||||
|
||||
// 返回文件下载响应
|
||||
return $documentService->downloadDocument($record, $user);
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()
|
||||
->title('下载失败')
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
])
|
||||
->paginated([10, 25, 50, 100])
|
||||
->defaultPaginationPageOption(25)
|
||||
->emptyStateHeading('暂无搜索结果')
|
||||
->emptyStateDescription('请输入搜索关键词并点击搜索按钮')
|
||||
->emptyStateIcon('heroicon-o-magnifying-glass');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格查询构建器
|
||||
*/
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
if (!$this->hasSearched || empty($this->searchQuery)) {
|
||||
// 如果还没有搜索或搜索关键词为空,返回空查询
|
||||
return Document::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
// 使用 DocumentSearchService 进行搜索
|
||||
$searchService = app(DocumentSearchService::class);
|
||||
$user = Auth::user();
|
||||
|
||||
$filters = [];
|
||||
if ($this->documentType) {
|
||||
$filters['type'] = $this->documentType;
|
||||
}
|
||||
if ($this->groupId) {
|
||||
$filters['group_id'] = $this->groupId;
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
$results = $searchService->search($this->searchQuery, $user, $filters);
|
||||
|
||||
// 获取搜索结果的 ID 列表
|
||||
$documentIds = $results->pluck('id')->toArray();
|
||||
|
||||
// 返回包含这些 ID 的查询构建器
|
||||
if (empty($documentIds)) {
|
||||
return Document::query()->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
return Document::query()
|
||||
->whereIn('id', $documentIds)
|
||||
->with(['group', 'uploader']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
public function search(): void
|
||||
{
|
||||
// 验证表单
|
||||
$data = $this->form->getState();
|
||||
|
||||
// 检查搜索关键词是否为空
|
||||
if (empty($data['searchQuery'])) {
|
||||
Notification::make()
|
||||
->title('请输入搜索关键词')
|
||||
->warning()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新搜索参数
|
||||
$this->searchQuery = $data['searchQuery'];
|
||||
$this->documentType = $data['documentType'];
|
||||
$this->groupId = $data['groupId'];
|
||||
$this->hasSearched = true;
|
||||
|
||||
// 重置表格分页
|
||||
$this->resetTable();
|
||||
|
||||
Notification::make()
|
||||
->title('搜索完成')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空搜索
|
||||
*/
|
||||
public function clearSearch(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'searchQuery' => '',
|
||||
'documentType' => null,
|
||||
'groupId' => null,
|
||||
]);
|
||||
|
||||
$this->searchQuery = null;
|
||||
$this->documentType = null;
|
||||
$this->groupId = null;
|
||||
$this->hasSearched = false;
|
||||
|
||||
$this->resetTable();
|
||||
|
||||
Notification::make()
|
||||
->title('已清空搜索')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取页面头部操作
|
||||
*/
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
301
app/Filament/Resources/DocumentResource.php
Normal file
301
app/Filament/Resources/DocumentResource.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\DocumentResource\Pages;
|
||||
use App\Filament\Resources\DocumentResource\RelationManagers;
|
||||
use App\Models\Document;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class DocumentResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Document::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
protected static ?string $navigationLabel = '文档管理';
|
||||
|
||||
protected static ?string $modelLabel = '文档';
|
||||
|
||||
protected static ?string $pluralModelLabel = '文档';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
// 应用 accessibleBy 作用域,确保用户只能看到有权限的文档
|
||||
$user = auth()->user();
|
||||
if ($user) {
|
||||
$query->accessibleBy($user);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('title')
|
||||
->label('文档标题')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('请输入文档标题')
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('文档描述')
|
||||
->rows(3)
|
||||
->maxLength(65535)
|
||||
->placeholder('请输入文档描述(可选)')
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\FileUpload::make('file')
|
||||
->label('文档文件')
|
||||
->required()
|
||||
->acceptedFileTypes(['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'])
|
||||
->maxSize(51200) // 50MB
|
||||
->disk('local')
|
||||
->directory('documents/' . date('Y/m/d'))
|
||||
->visibility('private')
|
||||
->downloadable()
|
||||
->preserveFilenames() // 保留原始文件名
|
||||
->helperText('仅支持 .doc 和 .docx 格式,最大 50MB')
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\Select::make('type')
|
||||
->label('文档类型')
|
||||
->required()
|
||||
->options([
|
||||
'global' => '全局知识库',
|
||||
'dedicated' => '专用知识库',
|
||||
])
|
||||
->default('global')
|
||||
->reactive()
|
||||
->afterStateUpdated(fn ($state, callable $set) =>
|
||||
$state === 'global' ? $set('group_id', null) : null
|
||||
)
|
||||
->helperText('全局知识库所有用户可见,专用知识库仅指定分组可见'),
|
||||
|
||||
Forms\Components\Select::make('group_id')
|
||||
->label('所属分组')
|
||||
->relationship('group', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(fn (Forms\Get $get): bool => $get('type') === 'dedicated')
|
||||
->visible(fn (Forms\Get $get): bool => $get('type') === 'dedicated')
|
||||
->helperText('专用知识库必须选择所属分组'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('title')
|
||||
->label('文档标题')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(50)
|
||||
->tooltip(function (Tables\Columns\TextColumn $column): ?string {
|
||||
$state = $column->getState();
|
||||
if (strlen($state) > 50) {
|
||||
return $state;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
Tables\Columns\TextColumn::make('type')
|
||||
->label('文档类型')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'global' => 'success',
|
||||
'dedicated' => 'warning',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'global' => '全局知识库',
|
||||
'dedicated' => '专用知识库',
|
||||
default => $state,
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('group.name')
|
||||
->label('所属分组')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->placeholder('—')
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('uploader.name')
|
||||
->label('上传者')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('file_size')
|
||||
->label('文件大小')
|
||||
->formatStateUsing(fn ($state): string => self::formatFileSize($state))
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('conversion_status')
|
||||
->label('转换状态')
|
||||
->badge()
|
||||
->color(fn (?string $state): string => match ($state) {
|
||||
'completed' => 'success',
|
||||
'processing' => 'info',
|
||||
'pending' => 'warning',
|
||||
'failed' => 'danger',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (?string $state): string => match ($state) {
|
||||
'completed' => '已完成',
|
||||
'processing' => '转换中',
|
||||
'pending' => '等待转换',
|
||||
'failed' => '转换失败',
|
||||
default => '未知',
|
||||
})
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('上传时间')
|
||||
->dateTime('Y年m月d日 H:i')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('更新时间')
|
||||
->dateTime('Y年m月d日 H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('type')
|
||||
->label('文档类型')
|
||||
->options([
|
||||
'global' => '全局知识库',
|
||||
'dedicated' => '专用知识库',
|
||||
])
|
||||
->placeholder('全部类型'),
|
||||
|
||||
Tables\Filters\SelectFilter::make('group_id')
|
||||
->label('所属分组')
|
||||
->relationship('group', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->placeholder('全部分组'),
|
||||
|
||||
Tables\Filters\SelectFilter::make('uploaded_by')
|
||||
->label('上传者')
|
||||
->relationship('uploader', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->placeholder('全部上传者'),
|
||||
|
||||
Tables\Filters\SelectFilter::make('conversion_status')
|
||||
->label('转换状态')
|
||||
->options([
|
||||
'pending' => '等待转换',
|
||||
'processing' => '转换中',
|
||||
'completed' => '已完成',
|
||||
'failed' => '转换失败',
|
||||
])
|
||||
->placeholder('全部状态'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('preview')
|
||||
->label('预览 Markdown')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('info')
|
||||
->visible(fn (Document $record): bool => $record->conversion_status === 'completed')
|
||||
->url(fn (Document $record): string => route('documents.preview', $record))
|
||||
->openUrlInNewTab()
|
||||
->tooltip(fn (Document $record): ?string =>
|
||||
$record->conversion_status !== 'completed'
|
||||
? '文档尚未完成转换'
|
||||
: null
|
||||
),
|
||||
Tables\Actions\Action::make('download')
|
||||
->label('下载')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('success')
|
||||
->action(function (Document $record) {
|
||||
$documentService = app(\App\Services\DocumentService::class);
|
||||
$user = auth()->user();
|
||||
|
||||
try {
|
||||
// 记录下载日志
|
||||
$documentService->logDownload($record, $user);
|
||||
|
||||
// 返回文件下载响应
|
||||
return $documentService->downloadDocument($record, $user);
|
||||
} catch (\Exception $e) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->danger()
|
||||
->title('下载失败')
|
||||
->body($e->getMessage())
|
||||
->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
Tables\Actions\ViewAction::make()
|
||||
->label('查看'),
|
||||
Tables\Actions\EditAction::make()
|
||||
->label('编辑'),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->label('删除'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make()
|
||||
->label('批量删除'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
public static function formatFileSize(?int $bytes): string
|
||||
{
|
||||
if ($bytes === null) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListDocuments::route('/'),
|
||||
'create' => Pages\CreateDocument::route('/create'),
|
||||
'view' => Pages\ViewDocument::route('/{record}'),
|
||||
'edit' => Pages\EditDocument::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DocumentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DocumentResource;
|
||||
use App\Services\DocumentService;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CreateDocument extends CreateRecord
|
||||
{
|
||||
protected static string $resource = DocumentResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
// 设置上传者为当前用户
|
||||
$data['uploaded_by'] = Auth::id();
|
||||
|
||||
// 如果是全局文档,确保 group_id 为 null
|
||||
if ($data['type'] === 'global') {
|
||||
$data['group_id'] = null;
|
||||
}
|
||||
|
||||
// 处理文件上传
|
||||
if (isset($data['file'])) {
|
||||
$filePath = $data['file'];
|
||||
|
||||
// 获取原始文件名(由于使用了 preserveFilenames(),basename 就是原始文件名)
|
||||
$originalFileName = basename($filePath);
|
||||
|
||||
// 保存文件信息
|
||||
$data['file_path'] = $filePath;
|
||||
$data['file_name'] = $originalFileName; // 保存原始文件名
|
||||
$data['file_size'] = Storage::disk('local')->size($filePath);
|
||||
$data['mime_type'] = Storage::disk('local')->mimeType($filePath);
|
||||
|
||||
// 移除临时的 file 字段
|
||||
unset($data['file']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
// 文档创建后,触发转换任务
|
||||
$conversionService = app(\App\Services\DocumentConversionService::class);
|
||||
$conversionService->queueConversion($this->record);
|
||||
}
|
||||
|
||||
protected function getCreatedNotification(): ?Notification
|
||||
{
|
||||
return Notification::make()
|
||||
->success()
|
||||
->title('文档上传成功')
|
||||
->body('文档已成功上传到知识库。');
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DocumentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DocumentResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EditDocument extends EditRecord
|
||||
{
|
||||
protected static string $resource = DocumentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make()
|
||||
->label('查看'),
|
||||
Actions\DeleteAction::make()
|
||||
->label('删除'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
// 将文件路径设置到 file 字段以便显示
|
||||
if (isset($data['file_path'])) {
|
||||
$data['file'] = $data['file_path'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// 如果是全局文档,确保 group_id 为 null
|
||||
if ($data['type'] === 'global') {
|
||||
$data['group_id'] = null;
|
||||
}
|
||||
|
||||
// 处理文件更新
|
||||
if (isset($data['file']) && $data['file'] !== $this->record->file_path) {
|
||||
$filePath = $data['file'];
|
||||
|
||||
// 删除旧的 Word 文件
|
||||
if ($this->record->file_path && Storage::disk('local')->exists($this->record->file_path)) {
|
||||
Storage::disk('local')->delete($this->record->file_path);
|
||||
}
|
||||
|
||||
// 删除旧的 Markdown 文件
|
||||
if ($this->record->markdown_path && Storage::disk('markdown')->exists($this->record->markdown_path)) {
|
||||
Storage::disk('markdown')->delete($this->record->markdown_path);
|
||||
}
|
||||
|
||||
// 获取原始文件名(由于使用了 preserveFilenames(),basename 就是原始文件名)
|
||||
$originalFileName = basename($filePath);
|
||||
|
||||
// 更新文件信息
|
||||
$data['file_path'] = $filePath;
|
||||
$data['file_name'] = $originalFileName; // 保存原始文件名
|
||||
$data['file_size'] = Storage::disk('local')->size($filePath);
|
||||
$data['mime_type'] = Storage::disk('local')->mimeType($filePath);
|
||||
|
||||
// 重置转换状态,准备重新转换
|
||||
$data['conversion_status'] = 'pending';
|
||||
$data['markdown_path'] = null;
|
||||
$data['markdown_preview'] = null;
|
||||
$data['conversion_error'] = null;
|
||||
}
|
||||
|
||||
// 移除临时的 file 字段
|
||||
unset($data['file']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
// 如果文档的转换状态是 pending,说明文件已更新,需要触发重新转换
|
||||
if ($this->record->conversion_status === 'pending') {
|
||||
$conversionService = app(\App\Services\DocumentConversionService::class);
|
||||
$conversionService->queueConversion($this->record);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSavedNotification(): ?Notification
|
||||
{
|
||||
return Notification::make()
|
||||
->success()
|
||||
->title('文档更新成功')
|
||||
->body('文档信息已成功更新。');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DocumentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DocumentResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDocuments extends ListRecords
|
||||
{
|
||||
protected static string $resource = DocumentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('上传文档'),
|
||||
];
|
||||
}
|
||||
}
|
||||
133
app/Filament/Resources/DocumentResource/Pages/ViewDocument.php
Normal file
133
app/Filament/Resources/DocumentResource/Pages/ViewDocument.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DocumentResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DocumentResource;
|
||||
use App\Services\DocumentPreviewService;
|
||||
use App\Services\DocumentService;
|
||||
use Filament\Actions;
|
||||
use Filament\Infolists\Components\Section;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewDocument extends ViewRecord
|
||||
{
|
||||
protected static string $resource = DocumentResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('preview')
|
||||
->label('预览 Markdown')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('info')
|
||||
->visible(fn (): bool => $this->record->conversion_status === 'completed')
|
||||
->url(fn (): string => route('documents.preview', $this->record))
|
||||
->openUrlInNewTab()
|
||||
->tooltip(fn (): ?string =>
|
||||
$this->record->conversion_status !== 'completed'
|
||||
? '文档尚未完成转换'
|
||||
: null
|
||||
),
|
||||
Actions\Action::make('download')
|
||||
->label('下载文档')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('success')
|
||||
->action(function () {
|
||||
$documentService = app(DocumentService::class);
|
||||
$user = auth()->user();
|
||||
|
||||
try {
|
||||
// 记录下载日志
|
||||
$documentService->logDownload($this->record, $user);
|
||||
|
||||
// 返回文件下载响应
|
||||
return $documentService->downloadDocument($this->record, $user);
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('下载失败')
|
||||
->body($e->getMessage())
|
||||
->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
Actions\EditAction::make()
|
||||
->label('编辑'),
|
||||
Actions\DeleteAction::make()
|
||||
->label('删除'),
|
||||
];
|
||||
}
|
||||
|
||||
public function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Section::make('文档信息')
|
||||
->schema([
|
||||
TextEntry::make('title')
|
||||
->label('文档标题')
|
||||
->size(TextEntry\TextEntrySize::Large)
|
||||
->weight('bold'),
|
||||
|
||||
TextEntry::make('description')
|
||||
->label('文档描述')
|
||||
->placeholder('无描述')
|
||||
->columnSpanFull(),
|
||||
|
||||
TextEntry::make('type')
|
||||
->label('文档类型')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'global' => 'success',
|
||||
'dedicated' => 'warning',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'global' => '全局知识库',
|
||||
'dedicated' => '专用知识库',
|
||||
default => $state,
|
||||
}),
|
||||
|
||||
TextEntry::make('group.name')
|
||||
->label('所属分组')
|
||||
->placeholder('—')
|
||||
->visible(fn ($record) => $record->type === 'dedicated'),
|
||||
|
||||
TextEntry::make('uploader.name')
|
||||
->label('上传者'),
|
||||
|
||||
TextEntry::make('file_name')
|
||||
->label('文件名'),
|
||||
|
||||
TextEntry::make('file_size')
|
||||
->label('文件大小')
|
||||
->formatStateUsing(fn ($state): string => DocumentResource::formatFileSize($state)),
|
||||
|
||||
TextEntry::make('created_at')
|
||||
->label('上传时间')
|
||||
->dateTime('Y年m月d日 H:i:s'),
|
||||
|
||||
TextEntry::make('updated_at')
|
||||
->label('更新时间')
|
||||
->dateTime('Y年m月d日 H:i:s'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make('文档预览')
|
||||
->schema([
|
||||
ViewEntry::make('preview')
|
||||
->label('')
|
||||
->view('filament.resources.document.preview')
|
||||
->viewData([
|
||||
'document' => $this->record,
|
||||
]),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(false),
|
||||
]);
|
||||
}
|
||||
}
|
||||
114
app/Filament/Resources/GroupResource.php
Normal file
114
app/Filament/Resources/GroupResource.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\GroupResource\Pages;
|
||||
use App\Filament\Resources\GroupResource\RelationManagers;
|
||||
use App\Models\Group;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class GroupResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Group::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-user-group';
|
||||
|
||||
protected static ?string $navigationLabel = '分组管理';
|
||||
|
||||
protected static ?string $modelLabel = '分组';
|
||||
|
||||
protected static ?string $pluralModelLabel = '分组';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('分组名称')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('请输入分组名称'),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('分组描述')
|
||||
->rows(3)
|
||||
->maxLength(65535)
|
||||
->placeholder('请输入分组描述(可选)')
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('分组名称')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('description')
|
||||
->label('分组描述')
|
||||
->limit(50)
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
Tables\Columns\TextColumn::make('users_count')
|
||||
->label('成员数量')
|
||||
->counts('users')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('创建时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('更新时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make()
|
||||
->label('查看'),
|
||||
Tables\Actions\EditAction::make()
|
||||
->label('编辑'),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->label('删除'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make()
|
||||
->label('批量删除'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\UsersRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListGroups::route('/'),
|
||||
'create' => Pages\CreateGroup::route('/create'),
|
||||
'edit' => Pages\EditGroup::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Filament/Resources/GroupResource/Pages/CreateGroup.php
Normal file
24
app/Filament/Resources/GroupResource/Pages/CreateGroup.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GroupResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GroupResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateGroup extends CreateRecord
|
||||
{
|
||||
protected static string $resource = GroupResource::class;
|
||||
|
||||
protected static ?string $title = '创建分组';
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getCreatedNotificationTitle(): ?string
|
||||
{
|
||||
return '分组创建成功';
|
||||
}
|
||||
}
|
||||
31
app/Filament/Resources/GroupResource/Pages/EditGroup.php
Normal file
31
app/Filament/Resources/GroupResource/Pages/EditGroup.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GroupResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GroupResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditGroup extends EditRecord
|
||||
{
|
||||
protected static string $resource = GroupResource::class;
|
||||
|
||||
protected static ?string $title = '编辑分组';
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->label('删除')
|
||||
->modalHeading('删除分组')
|
||||
->modalDescription('确定要删除此分组吗?此操作无法撤销。')
|
||||
->modalSubmitActionLabel('确认删除')
|
||||
->modalCancelActionLabel('取消'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSavedNotificationTitle(): ?string
|
||||
{
|
||||
return '分组更新成功';
|
||||
}
|
||||
}
|
||||
22
app/Filament/Resources/GroupResource/Pages/ListGroups.php
Normal file
22
app/Filament/Resources/GroupResource/Pages/ListGroups.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GroupResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GroupResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListGroups extends ListRecords
|
||||
{
|
||||
protected static string $resource = GroupResource::class;
|
||||
|
||||
protected static ?string $title = '分组列表';
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('创建分组'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\GroupResource\RelationManagers;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class UsersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'users';
|
||||
|
||||
protected static ?string $title = '分组成员';
|
||||
|
||||
protected static ?string $modelLabel = '用户';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('用户名称')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('email')
|
||||
->label('邮箱')
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('用户名称')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
->label('邮箱')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('加入时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\AttachAction::make()
|
||||
->label('添加成员')
|
||||
->preloadRecordSelect()
|
||||
->modalHeading('添加分组成员')
|
||||
->modalSubmitActionLabel('添加')
|
||||
->modalCancelActionLabel('取消'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\DetachAction::make()
|
||||
->label('移除')
|
||||
->modalHeading('移除分组成员')
|
||||
->modalDescription('确定要将此用户从分组中移除吗?')
|
||||
->modalSubmitActionLabel('确认移除')
|
||||
->modalCancelActionLabel('取消'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make()
|
||||
->label('批量移除')
|
||||
->modalHeading('批量移除分组成员')
|
||||
->modalDescription('确定要将选中的用户从分组中移除吗?')
|
||||
->modalSubmitActionLabel('确认移除')
|
||||
->modalCancelActionLabel('取消'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
129
app/Filament/Resources/UserResource.php
Normal file
129
app/Filament/Resources/UserResource.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Filament\Resources\UserResource\RelationManagers;
|
||||
use App\Models\User;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-users';
|
||||
|
||||
protected static ?string $navigationLabel = '用户管理';
|
||||
|
||||
protected static ?string $modelLabel = '用户';
|
||||
|
||||
protected static ?string $pluralModelLabel = '用户';
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('用户名称')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('请输入用户名称'),
|
||||
Forms\Components\TextInput::make('email')
|
||||
->label('邮箱')
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('请输入邮箱地址'),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->label('密码')
|
||||
->password()
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->minLength(8)
|
||||
->placeholder('请输入密码(至少8位)')
|
||||
->helperText('编辑时留空表示不修改密码'),
|
||||
Forms\Components\Select::make('groups')
|
||||
->label('所属分组')
|
||||
->multiple()
|
||||
->relationship('groups', 'name')
|
||||
->preload()
|
||||
->placeholder('请选择用户所属的分组')
|
||||
->helperText('用户可以属于多个分组'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('用户名称')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
->label('邮箱')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('groups.name')
|
||||
->label('所属分组')
|
||||
->badge()
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('创建时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('更新时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make()
|
||||
->label('查看'),
|
||||
Tables\Actions\EditAction::make()
|
||||
->label('编辑'),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->label('删除'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make()
|
||||
->label('批量删除'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\GroupsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'create' => Pages\CreateUser::route('/create'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
22
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateUser extends CreateRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getCreatedNotificationTitle(): ?string
|
||||
{
|
||||
return '用户创建成功';
|
||||
}
|
||||
}
|
||||
29
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
29
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->label('删除')
|
||||
->modalHeading('删除用户')
|
||||
->modalDescription('确定要删除此用户吗?此操作无法撤销。')
|
||||
->modalSubmitActionLabel('确认删除')
|
||||
->modalCancelActionLabel('取消'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSavedNotificationTitle(): ?string
|
||||
{
|
||||
return '用户更新成功';
|
||||
}
|
||||
}
|
||||
20
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
20
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('创建用户'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\RelationManagers;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class GroupsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'groups';
|
||||
|
||||
protected static ?string $title = '用户分组';
|
||||
|
||||
protected static ?string $modelLabel = '分组';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('分组名称')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('分组描述')
|
||||
->rows(3)
|
||||
->maxLength(65535),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('分组名称')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('description')
|
||||
->label('分组描述')
|
||||
->limit(50)
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('加入时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\AttachAction::make()
|
||||
->label('添加分组')
|
||||
->preloadRecordSelect()
|
||||
->modalHeading('添加用户到分组')
|
||||
->modalSubmitActionLabel('添加')
|
||||
->modalCancelActionLabel('取消'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\DetachAction::make()
|
||||
->label('移除')
|
||||
->modalHeading('移除用户分组')
|
||||
->modalDescription('确定要将此用户从该分组中移除吗?')
|
||||
->modalSubmitActionLabel('确认移除')
|
||||
->modalCancelActionLabel('取消'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DetachBulkAction::make()
|
||||
->label('批量移除')
|
||||
->modalHeading('批量移除用户分组')
|
||||
->modalDescription('确定要将此用户从选中的分组中移除吗?')
|
||||
->modalSubmitActionLabel('确认移除')
|
||||
->modalCancelActionLabel('取消'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user