diff --git a/.DS_Store b/.DS_Store index 0171a60..e28070c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/Livewire/Admin/ActivityLogTable.php b/app/Livewire/Admin/ActivityLogTable.php new file mode 100644 index 0000000..1dd0b4a --- /dev/null +++ b/app/Livewire/Admin/ActivityLogTable.php @@ -0,0 +1,97 @@ +showCheckBox(); + + return [ + PowerGrid::header() + ->showSearchInput(), + PowerGrid::footer() + ->showPerPage() + ->showRecordCount(), + ]; + } + + public function datasource(): Builder + { + return Activity::with(['causer'])->latest(); + } + + public function relationSearch(): array + { + return []; + } + + public function fields(): PowerGridFields + { + return PowerGrid::fields() + ->add('id') + ->add('created_at_formatted', fn (Activity $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s')) + ->add('causer_name', fn (Activity $model) => optional($model->causer)->name) + ->add('subject_type_label', fn (Activity $model) => class_basename($model->subject_type)) + ->add('subject_type') + ->add('subject_id') + ->add('description') + ->add('log_name') + ->add('changes', function (Activity $model) { + $old = $model->properties['old'] ?? []; + $new = $model->properties['attributes'] ?? []; + + $changes = []; + + foreach ($new as $key => $newValue) { + if (in_array($key, ['updated_at', 'created_at'])) continue; + $oldValue = $old[$key] ?? '(空)'; + if ($newValue != $oldValue) { + $changes[] = "{$key}: {$oldValue} → {$newValue}"; + } + } + + return implode('
', $changes); + }) + ; + + } + + public function columns(): array + { + $column=[]; + $column[]=Column::make('時間', 'created_at_formatted', 'created_at')->sortable()->searchable(); + $column[]=Column::make('操作者', 'causer.name')->sortable()->searchable()->bodyAttribute('whitespace-nowrap'); + $column[]=Column::make('模型', 'subject_type_label')->sortable()->searchable(); + $column[]=Column::make('模型 ID', 'subject_id')->sortable()->searchable(); + $column[]=Column::make('動作', 'description')->sortable()->searchable(); + $column[]=Column::make('變更內容', 'changes')->sortable(false)->searchable(false)->bodyAttribute('whitespace-normal text-sm text-gray-700'); + $column[]=Column::make('Log 名稱', 'log_name')->sortable()->searchable(); + return $column; + } + + public function filters(): array + { + return [ + Filter::datetimepicker('created_at'), + ]; + } + + + + +} diff --git a/app/Models/Artist.php b/app/Models/Artist.php index de7989d..554c107 100644 --- a/app/Models/Artist.php +++ b/app/Models/Artist.php @@ -6,12 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Helpers\ChineseNameConverter; use App\Helpers\ChineseStrokesConverter; - +use App\Traits\LogsModelActivity; class Artist extends Model { /** @use HasFactory<\Database\Factories\ArtistFactory> */ - use HasFactory; + use HasFactory, LogsModelActivity; protected $fillable = [ 'category', @@ -29,7 +29,7 @@ class Artist extends Model 'category' => \App\Enums\ArtistCategory::class, ]; } - + public function songs() { return $this->belongsToMany(Song::class); } diff --git a/app/Models/Branch.php b/app/Models/Branch.php index 48de8f2..de0c80e 100644 --- a/app/Models/Branch.php +++ b/app/Models/Branch.php @@ -4,15 +4,17 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use App\Traits\LogsModelActivity; class Branch extends Model { /** @use HasFactory<\Database\Factories\ArtistFactory> */ - use HasFactory; - + use HasFactory, LogsModelActivity; + protected $fillable = [ 'name', 'external_ip', + 'enable', ]; public function rooms() { diff --git a/app/Models/Room.php b/app/Models/Room.php index ad8c5df..53aea8f 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -4,11 +4,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use App\Traits\LogsModelActivity; class Room extends Model { /** @use HasFactory<\Database\Factories\ArtistFactory> */ - use HasFactory; + use HasFactory, LogsModelActivity; protected $casts = [ 'started_at' => 'datetime', diff --git a/app/Models/Song.php b/app/Models/Song.php index 762e330..83dda1e 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -6,11 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use App\Helpers\ChineseNameConverter; use App\Helpers\ChineseStrokesConverter; +use App\Traits\LogsModelActivity; class Song extends Model { /** @use HasFactory<\Database\Factories\SongFactory> */ - use HasFactory; + use HasFactory, LogsModelActivity; protected $fillable = [ 'id', diff --git a/app/Models/User.php b/app/Models/User.php index 79be10f..c6057b0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,11 +7,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Spatie\Permission\Traits\HasRoles; +use App\Traits\LogsModelActivity; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, HasRoles; + use HasFactory, Notifiable, HasRoles, LogsModelActivity; /** * The attributes that are mass assignable. diff --git a/app/Traits/LogsModelActivity.php b/app/Traits/LogsModelActivity.php new file mode 100644 index 0000000..7c68b62 --- /dev/null +++ b/app/Traits/LogsModelActivity.php @@ -0,0 +1,23 @@ +useLogName(strtolower(class_basename(static::class))) + ->logOnly($this->getFillable()) + ->logOnlyDirty() + ->dontSubmitEmptyLogs() + ->setDescriptionForEvent(function (string $eventName) { + return class_basename(static::class) . " 已 {$eventName}"; + }); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index cb45c53..5a85aa9 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "overtrue/php-opencc": "^1.2", "overtrue/pinyin": "^5.3", "power-components/livewire-powergrid": "^6.3", + "spatie/laravel-activitylog": "^4.10", "spatie/laravel-permission": "^6.17", "wire-elements/modal": "^2.0", "wireui/wireui": "^2.4" diff --git a/composer.lock b/composer.lock index c1a859c..8f10ccb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d5d6344c1be46378bdaa0bfecd2e300e", + "content-hash": "54bbfde233e690f726d54157b29f12a3", "packages": [ { "name": "brick/math", @@ -4346,6 +4346,97 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "spatie/laravel-activitylog", + "version": "4.10.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/466f30f7245fe3a6e328ad5e6812bd43b4bddea5", + "reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-10T15:38:25+00:00" + }, { "name": "spatie/laravel-package-tools", "version": "1.92.4", diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 0000000..f1262f5 --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,52 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + + /* + * When the clean-command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'delete_records_older_than_days' => 365, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the current Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject returns soft deleted models. + */ + 'subject_returns_soft_deleted_models' => false, + + /* + * This model will be used to log activity. + * It should implement the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => \Spatie\Activitylog\Models\Activity::class, + + /* + * This is the name of the table that will be created by the migration and + * used by the Activity model shipped with this package. + */ + 'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'), + + /* + * This is the database connection that will be used by the migration and + * the Activity model shipped with this package. In case it's not set + * Laravel's database.default will be used instead. + */ + 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'), +]; diff --git a/config/database.php b/config/database.php index 3082351..8910562 100644 --- a/config/database.php +++ b/config/database.php @@ -41,17 +41,6 @@ return [ 'journal_mode' => null, 'synchronous' => null, ], - - 'KTVsqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DB_URL'), - 'database' => env('SQLITE_DB', database_path('KSongDatabase.db')), - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - 'busy_timeout' => null, - 'journal_mode' => null, - 'synchronous' => null, - ], 'mysql' => [ 'driver' => 'mysql', diff --git a/database/migrations/2025_04_22_193851_create_activity_log_table.php b/database/migrations/2025_04_22_193851_create_activity_log_table.php new file mode 100644 index 0000000..7c05bc8 --- /dev/null +++ b/database/migrations/2025_04_22_193851_create_activity_log_table.php @@ -0,0 +1,27 @@ +create(config('activitylog.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->nullableMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->timestamps(); + $table->index('log_name'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + } +} diff --git a/database/migrations/2025_04_22_193852_add_event_column_to_activity_log_table.php b/database/migrations/2025_04_22_193852_add_event_column_to_activity_log_table.php new file mode 100644 index 0000000..7b797fd --- /dev/null +++ b/database/migrations/2025_04_22_193852_add_event_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->string('event')->nullable()->after('subject_type'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('event'); + }); + } +} diff --git a/database/migrations/2025_04_22_193853_add_batch_uuid_column_to_activity_log_table.php b/database/migrations/2025_04_22_193853_add_batch_uuid_column_to_activity_log_table.php new file mode 100644 index 0000000..8f7db66 --- /dev/null +++ b/database/migrations/2025_04_22_193853_add_batch_uuid_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->uuid('batch_uuid')->nullable()->after('properties'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('batch_uuid'); + }); + } +} diff --git a/resources/views/livewire/admin/activity-log.blade.php b/resources/views/livewire/admin/activity-log.blade.php new file mode 100644 index 0000000..560ce2d --- /dev/null +++ b/resources/views/livewire/admin/activity-log.blade.php @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/views/livewire/layout/admin/sidebar.blade.php b/resources/views/livewire/layout/admin/sidebar.blade.php index 34e6f26..d0efb7b 100644 --- a/resources/views/livewire/layout/admin/sidebar.blade.php +++ b/resources/views/livewire/layout/admin/sidebar.blade.php @@ -3,6 +3,7 @@ $menus = [ ['label' => 'Dashboard', 'route' => 'admin.dashboard', 'icon' => 'home', 'permission' => null], + ['label' => 'ActivityLog', 'route' => 'admin.activity-log', 'icon' => 'clock', 'permission' => null], ['label' => 'Role', 'route' => 'admin.roles', 'icon' => 'user-circle', 'permission' => 'role-list'], ['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'], ['label' => 'Artist', 'route' => 'admin.artists', 'icon' => 'musical-note', 'permission' => 'song-list'], diff --git a/routes/web.php b/routes/web.php index 660026d..3e9cd7a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,7 +21,7 @@ require __DIR__.'/auth.php'; Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () { Route::get('/dashboard', AdminDashboard::class)->name('dashboard'); - + Route::get('/activity-log', function () {return view('livewire.admin.activity-log');})->name('activity-log'); Route::get('/roles', function () {return view('livewire.admin.roles');})->name('roles'); Route::get('/users', function () {return view('livewire.admin.users');})->name('users'); Route::get('/artists', function () {return view('livewire.admin.artists');})->name('artists'); diff --git a/開發手冊.ini b/開發手冊.ini index 2ced5c6..2fc1393 100644 --- a/開發手冊.ini +++ b/開發手冊.ini @@ -61,6 +61,7 @@ npm install && npm run build php artisan convert:unihan-strokes \ --input=resources/data/Unihan_IRGSources.txt \ --output=resources/data/unihan_strokes.php + php artisan cache:forget unihan_strokes @@ -75,6 +76,13 @@ php artisan vendor:publish --tag=livewire-powergrid-config composer require overtrue/php-opencc -vvv composer require overtrue/pinyin +操作記錄 +composer require spatie/laravel-activitylog +php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations" +php artisan migrate +php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config" +php artisan make:model ActivityLog + 建立分頁table php artisan powergrid:create