feat: 实现操作日志管理界面
- ActivityLogResource: Filament 资源类 - 只读模式(禁用创建、编辑、删除) - 表格列:时间、用户、操作类型、对象、详情 - 按时间倒序排序 - 支持多维度筛选(时间范围、操作类型、用户、对象类型) - 集成导出功能(Excel/CSV) - ViewActivityLog: 日志详情页面 - 完整的变更信息展示 - JSON diff 对比视图 - 支持查看原始 JSON 数据 - activity-log-diff.blade.php: Diff 对比组件 - 字段级别的变更对比 - 使用颜色区分新旧值(绿色/红色) - 支持 JSON 数据格式化显示
This commit is contained in:
249
app/Filament/Resources/ActivityLogResource.php
Normal file
249
app/Filament/Resources/ActivityLogResource.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ActivityLogResource\Pages;
|
||||
use App\Exports\ActivityLogExport;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class ActivityLogResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Activity::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list';
|
||||
|
||||
protected static ?string $navigationLabel = '操作日志';
|
||||
|
||||
protected static ?string $modelLabel = '操作日志';
|
||||
|
||||
protected static ?string $pluralModelLabel = '操作日志';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
// 禁用创建功能
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->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}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user