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'), ]; } }