diff --git a/app/Filament/Resources/ActivityLogResource.php b/app/Filament/Resources/ActivityLogResource.php new file mode 100644 index 0000000..7669469 --- /dev/null +++ b/app/Filament/Resources/ActivityLogResource.php @@ -0,0 +1,249 @@ +schema([ + // 只读资源,不需要表单 + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('created_at') + ->label('操作时间') + ->dateTime('Y-m-d H:i:s') + ->sortable() + ->searchable(), + + Tables\Columns\TextColumn::make('causer.name') + ->label('操作用户') + ->searchable() + ->sortable() + ->default('系统') + ->placeholder('系统'), + + Tables\Columns\TextColumn::make('description') + ->label('操作类型') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'created' => 'success', + 'updated' => 'info', + 'deleted' => 'danger', + default => 'gray', + }) + ->formatStateUsing(fn (string $state): string => match ($state) { + 'created' => '创建', + 'updated' => '更新', + 'deleted' => '删除', + default => $state, + }) + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('subject_type') + ->label('操作对象') + ->formatStateUsing(function (?string $state): string { + if (!$state) { + return '-'; + } + // 提取类名 + $className = class_basename($state); + // 转换为中文 + return match ($className) { + 'SystemSetting' => '系统设置', + 'User' => '用户', + 'Document' => '文档', + 'Group' => '分组', + 'Terminal' => '终端', + 'SopTemplate' => 'SOP模板', + default => $className, + }; + }) + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('subject_id') + ->label('对象ID') + ->searchable() + ->sortable() + ->toggleable(), + + Tables\Columns\TextColumn::make('properties') + ->label('详情') + ->limit(50) + ->tooltip(function (Tables\Columns\TextColumn $column): ?string { + $state = $column->getState(); + if (is_array($state)) { + return json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + return null; + }) + ->formatStateUsing(function ($state): string { + if (is_array($state)) { + $summary = []; + if (isset($state['attributes'])) { + $summary[] = '新值: ' . count($state['attributes']) . '项'; + } + if (isset($state['old'])) { + $summary[] = '旧值: ' . count($state['old']) . '项'; + } + return implode(', ', $summary) ?: '无变更'; + } + return '-'; + }) + ->toggleable(), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + // 时间范围筛选 + Tables\Filters\Filter::make('created_at') + ->form([ + \Filament\Forms\Components\DatePicker::make('created_from') + ->label('开始时间') + ->placeholder('选择开始时间'), + \Filament\Forms\Components\DatePicker::make('created_until') + ->label('结束时间') + ->placeholder('选择结束时间'), + ]) + ->query(function ($query, array $data) { + return $query + ->when($data['created_from'], fn ($query, $date) => $query->whereDate('created_at', '>=', $date)) + ->when($data['created_until'], fn ($query, $date) => $query->whereDate('created_at', '<=', $date)); + }) + ->indicateUsing(function (array $data): array { + $indicators = []; + if ($data['created_from'] ?? null) { + $indicators[] = Tables\Filters\Indicator::make('开始时间: ' . \Carbon\Carbon::parse($data['created_from'])->format('Y-m-d')) + ->removeField('created_from'); + } + if ($data['created_until'] ?? null) { + $indicators[] = Tables\Filters\Indicator::make('结束时间: ' . \Carbon\Carbon::parse($data['created_until'])->format('Y-m-d')) + ->removeField('created_until'); + } + return $indicators; + }), + + // 操作类型筛选 + Tables\Filters\SelectFilter::make('description') + ->label('操作类型') + ->options([ + 'created' => '创建', + 'updated' => '更新', + 'deleted' => '删除', + ]) + ->placeholder('全部类型'), + + // 用户筛选 + Tables\Filters\SelectFilter::make('causer_id') + ->label('操作用户') + ->relationship('causer', 'name') + ->searchable() + ->preload() + ->placeholder('全部用户'), + + // 对象类型筛选 + Tables\Filters\SelectFilter::make('subject_type') + ->label('对象类型') + ->options([ + 'App\\Models\\SystemSetting' => '系统设置', + 'App\\Models\\User' => '用户', + 'App\\Models\\Document' => '文档', + 'App\\Models\\Group' => '分组', + 'App\\Models\\Terminal' => '终端', + 'App\\Models\\SopTemplate' => 'SOP模板', + ]) + ->placeholder('全部类型'), + ]) + ->actions([ + Tables\Actions\ViewAction::make() + ->label('查看'), + ]) + ->bulkActions([ + // 不允许批量操作 + ]) + ->headerActions([ + // 导出操作 + Tables\Actions\Action::make('export') + ->label('导出日志') + ->icon('heroicon-o-arrow-down-tray') + ->form([ + \Filament\Forms\Components\Select::make('format') + ->label('导出格式') + ->options([ + 'xlsx' => 'Excel (XLSX)', + 'csv' => 'CSV', + ]) + ->default('xlsx') + ->required(), + ]) + ->action(function (array $data, Table $table) { + // 获取当前筛选后的查询 + $query = $table->getFilteredTableQuery(); + + // 导出文件名 + $filename = '操作日志_' . now()->format('YmdHis'); + + // 根据格式导出 + if ($data['format'] === 'csv') { + return Excel::download( + new ActivityLogExport($query), + $filename . '.csv', + \Maatwebsite\Excel\Excel::CSV + ); + } + + return Excel::download( + new ActivityLogExport($query), + $filename . '.xlsx' + ); + }) + ->color('success') + ->requiresConfirmation() + ->modalHeading('导出操作日志') + ->modalDescription('将根据当前筛选条件导出日志数据') + ->modalSubmitActionLabel('确认导出'), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListActivityLogs::route('/'), + 'view' => Pages\ViewActivityLog::route('/{record}'), + ]; + } +} diff --git a/app/Filament/Resources/ActivityLogResource/Pages/ListActivityLogs.php b/app/Filament/Resources/ActivityLogResource/Pages/ListActivityLogs.php new file mode 100644 index 0000000..c9d26ff --- /dev/null +++ b/app/Filament/Resources/ActivityLogResource/Pages/ListActivityLogs.php @@ -0,0 +1,18 @@ +schema([ + Infolists\Components\Section::make('基本信息') + ->schema([ + Infolists\Components\TextEntry::make('created_at') + ->label('操作时间') + ->dateTime('Y-m-d H:i:s'), + + Infolists\Components\TextEntry::make('causer.name') + ->label('操作用户') + ->default('系统'), + + Infolists\Components\TextEntry::make('description') + ->label('操作类型') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'created' => 'success', + 'updated' => 'info', + 'deleted' => 'danger', + default => 'gray', + }) + ->formatStateUsing(fn (string $state): string => match ($state) { + 'created' => '创建', + 'updated' => '更新', + 'deleted' => '删除', + default => $state, + }), + + Infolists\Components\TextEntry::make('subject_type') + ->label('操作对象类型') + ->formatStateUsing(function (?string $state): string { + if (!$state) { + return '-'; + } + $className = class_basename($state); + return match ($className) { + 'SystemSetting' => '系统设置', + 'User' => '用户', + 'Document' => '文档', + 'Group' => '分组', + 'Terminal' => '终端', + 'SopTemplate' => 'SOP模板', + default => $className, + }; + }), + + Infolists\Components\TextEntry::make('subject_id') + ->label('对象ID'), + + Infolists\Components\TextEntry::make('log_name') + ->label('日志名称') + ->default('default'), + ]) + ->columns(2), + + Infolists\Components\Section::make('变更详情') + ->schema([ + Infolists\Components\ViewEntry::make('properties') + ->label('') + ->view('filament.infolists.components.activity-log-diff') + ->columnSpanFull() + ->visible(fn ($record) => !empty($record->properties)), + ]) + ->collapsible(), + ]); + } +} diff --git a/resources/views/filament/infolists/components/activity-log-diff.blade.php b/resources/views/filament/infolists/components/activity-log-diff.blade.php new file mode 100644 index 0000000..e382fb6 --- /dev/null +++ b/resources/views/filament/infolists/components/activity-log-diff.blade.php @@ -0,0 +1,103 @@ +
+ @php + $properties = $getState(); + $old = $properties['old'] ?? []; + $attributes = $properties['attributes'] ?? []; + + // 获取所有键 + $allKeys = array_unique(array_merge(array_keys($old), array_keys($attributes))); + @endphp + + @if(empty($allKeys)) +
+ 无变更数据 +
+ @else +
+ + + + + + + + + + @foreach($allKeys as $key) + @php + $oldValue = $old[$key] ?? null; + $newValue = $attributes[$key] ?? null; + $hasChanged = $oldValue !== $newValue; + @endphp + + + + + + @endforeach + +
+ 字段 + + 旧值 + + 新值 +
+ {{ $key }} + @if($hasChanged) + + 已变更 + + @endif + + @if($oldValue !== null) +
+ @if(is_array($oldValue) || is_object($oldValue)) +
{{ json_encode($oldValue, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) }}
+ @else + {{ $oldValue }} + @endif +
+ @else + - + @endif +
+ @if($newValue !== null) +
+ @if(is_array($newValue) || is_object($newValue)) +
{{ json_encode($newValue, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) }}
+ @else + {{ $newValue }} + @endif +
+ @else + - + @endif +
+
+ + @if(!empty($old) || !empty($attributes)) +
+
+ + 查看原始 JSON 数据 + +
+ @if(!empty($old)) +
+
旧值 (JSON):
+
{{ json_encode($old, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) }}
+
+ @endif + @if(!empty($attributes)) +
+
新值 (JSON):
+
{{ json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) }}
+
+ @endif +
+
+
+ @endif + @endif +