加入 ⻆色權限控制

修正 'n' => 'ㄣ' 轉值問題
加入 歌手搜尋功能
DB 開 點播次數欄位
20250505
This commit is contained in:
allen.yan 2025-05-05 11:22:40 +08:00
parent 077d418357
commit d9924bf05b
31 changed files with 621 additions and 442 deletions

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum ArtistCategory: string enum ArtistCategory: string
{ {
use HasLabels;
case Unset = '未定義'; case Unset = '未定義';
case Male = '男'; case Male = '男';
case Female = '女'; case Female = '女';
@ -23,9 +27,4 @@ enum ArtistCategory: string
self::Unset => __('enums.Unset'), self::Unset => __('enums.Unset'),
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum SongLanguageType: string enum SongLanguageType: string
{ {
use HasLabels;
case Unset = '未定義'; case Unset = '未定義';
case Mandarin = '國語'; case Mandarin = '國語';
case Taiwanese = '台語'; case Taiwanese = '台語';
@ -31,9 +35,4 @@ enum SongLanguageType: string
self::Other => __('enums.song.LanguageType.Other'), self::Other => __('enums.song.LanguageType.Other'),
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum SongSituation: string enum SongSituation: string
{ {
use HasLabels;
case Unset = '未定義'; case Unset = '未定義';
case Romantic = '浪漫'; case Romantic = '浪漫';
case Soft = '柔和'; case Soft = '柔和';
@ -22,8 +26,4 @@ enum SongSituation: string
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -0,0 +1,20 @@
<?php
namespace App\Enums\Traits;
use Illuminate\Support\Collection;
trait HasLabels
{
public static function options(): Collection
{
return collect(self::cases())->mapWithKeys(function (self $case) {
return [$case->value => $case->labels()];
});
}
public function labelPowergridFilter(): string
{
return $this->labels();
}
}

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum UserGender: string enum UserGender: string
{ {
use HasLabels;
case Male = 'male'; case Male = 'male';
case Female = 'female'; case Female = 'female';
case Other = 'other'; case Other = 'other';
@ -20,8 +24,4 @@ enum UserGender: string
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -2,8 +2,12 @@
namespace App\Enums; namespace App\Enums;
use App\Enums\Traits\HasLabels;
enum UserStatus: int enum UserStatus: int
{ {
use HasLabels;
case Active = 0; // 正常 case Active = 0; // 正常
case Suspended = 1; // 停權 case Suspended = 1; // 停權
case Deleting = 2; // 刪除中 case Deleting = 2; // 刪除中
@ -17,9 +21,5 @@ enum UserStatus: int
self::Deleting => __('enums.user.status.Deleting'), self::Deleting => __('enums.user.status.Deleting'),
}; };
} }
public function labelPowergridFilter(): String
{
return $this -> labels();
}
} }

View File

@ -8,7 +8,7 @@ use Overtrue\Pinyin\Pinyin;
class ChineseNameConverter class ChineseNameConverter
{ {
public static array $pinyinToZhuyinMap = [ public static array $pinyinToZhuyinMap = [
'a' => 'ㄚ' ,'o' => 'ㄛ' ,'e' => 'ㄜ' ,'er' => 'ㄦ','ai' => 'ㄞ' ,'ei' => 'ㄟ' ,'ao' => 'ㄠ' ,'ou' => 'ㄡ' ,'an' => 'ㄢ' ,'en' => 'ㄣ' 'a' => 'ㄚ' ,'o' => 'ㄛ' ,'e' => 'ㄜ' ,'er' => 'ㄦ','ai' => 'ㄞ' ,'ei' => 'ㄟ' ,'ao' => 'ㄠ' ,'ou' => 'ㄡ' ,'an' => 'ㄢ' ,'n' => 'ㄣ','en' => 'ㄣ'
,'ang' => 'ㄤ' ,'eng' => 'ㄥ' ,'yi' => 'ㄧ' ,'ya' => 'ㄧㄚ' ,'yo' => 'ㄧㄛ' ,'ye' => 'ㄧㄝ' ,'yai' => 'ㄧㄞ' ,'yao' => 'ㄧㄠ' ,'ang' => 'ㄤ' ,'eng' => 'ㄥ' ,'yi' => 'ㄧ' ,'ya' => 'ㄧㄚ' ,'yo' => 'ㄧㄛ' ,'ye' => 'ㄧㄝ' ,'yai' => 'ㄧㄞ' ,'yao' => 'ㄧㄠ'
,'you' => 'ㄧㄡ' ,'yan' => 'ㄧㄢ' ,'yin' => 'ㄧㄣ' ,'yang' => 'ㄧㄤ' ,'ying' => 'ㄧㄥ' ,'wu' => 'ㄨ' ,'wa' => 'ㄨㄚ' ,'wo' => 'ㄨㄛ' ,'you' => 'ㄧㄡ' ,'yan' => 'ㄧㄢ' ,'yin' => 'ㄧㄣ' ,'yang' => 'ㄧㄤ' ,'ying' => 'ㄧㄥ' ,'wu' => 'ㄨ' ,'wa' => 'ㄨㄚ' ,'wo' => 'ㄨㄛ'
,'wai' => 'ㄨㄞ' ,'wei' => 'ㄨㄟ' ,'wan' => 'ㄨㄢ' ,'wen' => 'ㄨㄣ' ,'wang' => 'ㄨㄤ' ,'weng' => 'ㄨㄥ' ,'yu' => 'ㄩ' ,'yue' => 'ㄩㄝ' ,'wai' => 'ㄨㄞ' ,'wei' => 'ㄨㄟ' ,'wan' => 'ㄨㄢ' ,'wen' => 'ㄨㄣ' ,'wang' => 'ㄨㄤ' ,'weng' => 'ㄨㄥ' ,'yu' => 'ㄩ' ,'yue' => 'ㄩㄝ'
@ -79,6 +79,7 @@ class ChineseNameConverter
'ū'=>'u', 'ú'=>'u', 'ǔ'=>'u', 'ù'=>'u', 'ū'=>'u', 'ú'=>'u', 'ǔ'=>'u', 'ù'=>'u',
//'ǖ'=>'ü','ǘ'=>'ü','ǚ'=>'ü','ǜ'=>'ü', //'ǖ'=>'ü','ǘ'=>'ü','ǚ'=>'ü','ǜ'=>'ü',
'ǖ'=>'u','ǘ'=>'u','ǚ'=>'u','ǜ'=>'u', 'ǖ'=>'u','ǘ'=>'u','ǚ'=>'u','ǜ'=>'u',
'ǹ'=>'n',
]; ];
return strtr($pinyin, $map); return strtr($pinyin, $map);

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Models\Artist;
use Illuminate\Http\Request;
class ArtistController extends Controller
{
public function search(Request $request)
{
$query = Artist::query();
if ($request->filled('selected')) {
// 取得已選擇的項目(初始化時用)
return $query->whereIn('id', (array) $request->input('selected'))
->get(['id', 'name']);
}
if ($request->filled('search')) {
// 關鍵字搜尋(選單輸入文字時用)
$search = $request->input('search');
$query->where('name', 'like', "%{$search}%");
}
return $query->limit(10)->get(['id', 'name']);
}
}

View File

@ -2,7 +2,12 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
abstract class Controller use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{ {
// use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
} }

View File

@ -6,6 +6,7 @@ use App\Models\Artist;
use App\Enums\ArtistCategory; use App\Enums\ArtistCategory;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Button;
@ -22,6 +23,10 @@ use Livewire\Attributes\On;
final class ArtistTable extends PowerGridComponent final class ArtistTable extends PowerGridComponent
{ {
public string $tableName = 'artist-table'; public string $tableName = 'artist-table';
public bool $canCreate;
public bool $canEdit;
public bool $canDownload;
public bool $canDelect;
//public bool $deferLoading = true; //public bool $deferLoading = true;
//public string $loadingComponent = 'components.power-grid-loading'; //public string $loadingComponent = 'components.power-grid-loading';
@ -31,36 +36,47 @@ final class ArtistTable extends PowerGridComponent
public function boot(): void public function boot(): void
{ {
config(['livewire-powergrid.filter' => 'outside']); config(['livewire-powergrid.filter' => 'outside']);
//權限設定
$this->canCreate = Auth::user()?->can('song-edit') ?? false;
$this->canEdit = Auth::user()?->can('song-edit') ?? false;
$this->canDownload=Auth::user()?->can('song-delete') ?? false;
$this->canDelect = Auth::user()?->can('song-delete') ?? false;
} }
public function setUp(): array public function setUp(): array
{ {
//$this->showCheckBox(); if($this->canDownload || $this->canDelect){
$this->showCheckBox();
return [ }
//PowerGrid::exportable(fileName: 'artist-file')
// ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV), $actions = [];
PowerGrid::header() if($this->canDownload){
->withoutLoading() $actions[]=PowerGrid::exportable(fileName: 'artist-file')
->showToggleColumns() ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
//->showSoftDeletes() }
//->showSearchInput() $header = PowerGrid::header()
->includeViewOnTop('livewire.admin.artist-header') ->withoutLoading()
, ->showToggleColumns();
PowerGrid::footer() //->showSoftDeletes()
->showPerPage() //->showSearchInput()
->showRecordCount(), if($this->canCreate){
]; $header->includeViewOnTop('livewire.admin.artist-header') ;
}
$actions[]=$header;
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
return $actions;
} }
public function header(): array public function header(): array
{ {
return [ $actions = [];
/* Button::add('bulk-delete') if ($this->canDelect) {
$actions[]=Button::add('bulk-delete')
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)') ->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold']) ->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispat ch('bulkDelete.' . $this->tableName, []),*/ ->dispatch('bulkDelete.' . $this->tableName, []);
]; }
return $actions;
} }
public function datasource(): Builder public function datasource(): Builder
@ -75,19 +91,23 @@ final class ArtistTable extends PowerGridComponent
public function fields(): PowerGridFields public function fields(): PowerGridFields
{ {
$options = $this->categorySelectOptions();
return PowerGrid::fields() return PowerGrid::fields()
->add('id') ->add('id')
->add('category_str', function (Artist $model) use ($options){ ->add('category_str', function (Artist $model) {
return Blade::render( if ($this->canEdit) {
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>', return Blade::render(
[ '<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
'options' => $options, [
'modelId' => intval($model->id), 'options' => ArtistCategory::options(),
'fieldName'=>'category', 'modelId' => intval($model->id),
'selected' => $model->category->value 'fieldName'=>'category',
]); 'selected' => $model->category->value
]
);
}
// 沒有權限就顯示對應的文字
return $model->category->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
} ) } )
->add('name') ->add('name')
->add('simplified') ->add('simplified')
@ -99,51 +119,59 @@ final class ArtistTable extends PowerGridComponent
public function columns(): array public function columns(): array
{ {
return [ $column=[];
Column::make(__('artists.no'), 'id'), $column[]=Column::make(__('artists.no'), 'id');
Column::make(__('artists.category'),'category_str', 'artists.category')->searchable(), $column[]=Column::make(__('artists.category'),'category_str', 'artists.category')->searchable();
Column::make(__('artists.name'), 'name') $column[]=Column::make(__('artists.name'), 'name')->sortable()->searchable()
->sortable() ->editOnClick(
->searchable(), hasPermission: $this->canEdit,
dataField: 'name',
Column::make(__('artists.name.simplified'), 'simplified') fallback: 'N/A',
->sortable() saveOnMouseOut: true
->searchable() );
->hidden(true, false), $column[]=Column::make(__('artists.name.simplified'), 'simplified')->sortable()->searchable()->hidden(true, false);
$column[]=Column::make(__('artists.name.phinetic'), 'phonetic_abbr')->sortable()->searchable();
Column::make(__('artists.name.phinetic'), 'phonetic_abbr') $column[]=Column::make(__('artists.name.pinyin'), 'pinyin_abbr')->sortable()->searchable();
->sortable() $column[]=Column::make(__('artists.name.strokes'), 'strokes_abbr')->sortable()->searchable();
->searchable(), $column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
$column[]=Column::action(__('artists.actions'));
Column::make(__('artists.name.pinyin'), 'pinyin_abbr')
->sortable() return $column;
->searchable(),
Column::make(__('artists.name.strokes'), 'strokes_abbr')
->sortable()
->searchable(),
Column::make('Created at', 'created_at_formatted', 'created_at')
->sortable()
->hidden(true, false),
Column::action(__('artists.actions'))
];
} }
#[On('bulkDelete.{tableName}')]
public function categorySelectOptions():Collection public function bulkDelete(): void
{ {
return collect(ArtistCategory::cases())->mapWithKeys(function (ArtistCategory $case) { if ($this->canDelect) {
return [$case->value => $case->labels()]; $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
}); if($this->checkboxValues){
} Artist::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
}
}
}
#[On('categoryChanged')] #[On('categoryChanged')]
public function categoryChanged($value,$fieldName, $modelId): void public function categoryChanged($value,$fieldName, $modelId): void
{ {
// dd($value,$fieldName, $modelId); // dd($value,$fieldName, $modelId);
if($fieldName=='category'){ if ($fieldName == 'category' && $this->canEdit) {
Artist::find($modelId)?->update([$fieldName => $value]); $this->noUpdated($modelId,$fieldName,$value);
} }
} }
#[On('onUpdatedEditable')]
public function onUpdatedEditable($id, $field, $value): void
{
if ($field === 'name' && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
private function noUpdated($id,$field,$value){
$artist = Artist::find($id);
if ($artist) {
$artist->{$field} = $value;
$artist->save(); // 明確觸發 saving
}
}
public function filters(): array public function filters(): array
{ {
return [ return [
@ -157,32 +185,24 @@ final class ArtistTable extends PowerGridComponent
Filter::datetimepicker('created_at'), Filter::datetimepicker('created_at'),
]; ];
} }
/* #[On('bulkDelete.{tableName}')]
public function bulkDelete(): void
{
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
if($this->checkboxValues){
Artist::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
}
} */
public function actions(Artist $row): array public function actions(Artist $row): array
{ {
return [ $actions = [];
if ($this->canEdit) {
Button::add('edit') $actions[] =Button::add('edit')
->slot(__('artists.edit')) ->slot(__('artists.edit'))
->icon('solid-pencil-square') ->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.artist-form', 'openEditArtistModal', ['id' => $row->id]), ->dispatchTo('admin.artist-form', 'openEditArtistModal', ['id' => $row->id]);
/* Button::add('delete') }
if($this->canDelect){
$actions[] =Button::add('delete')
->slot(__('artists.delete')) ->slot(__('artists.delete'))
->icon('solid-trash') ->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.artist-form', 'deleteArtist', ['id' => $row->id]), */ ->dispatchTo('admin.artist-form', 'deleteArtist', ['id' => $row->id]);
]; }
return $actions;
} }

View File

@ -5,6 +5,7 @@ namespace App\Livewire\Admin;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column; use PowerComponents\LivewirePowerGrid\Column;
@ -16,25 +17,47 @@ use PowerComponents\LivewirePowerGrid\PowerGridComponent;
final class RoleTable extends PowerGridComponent final class RoleTable extends PowerGridComponent
{ {
public string $tableName = 'role-table'; public string $tableName = 'role-table';
public bool $canCreate;
public bool $canEdit;
public bool $canDelect;
public function boot(): void public function boot(): void
{ {
config(['livewire-powergrid.filter' => 'outside']); config(['livewire-powergrid.filter' => 'outside']);
//權限設定
$this->canCreate = Auth::user()?->can('role-edit') ?? false;
$this->canEdit = Auth::user()?->can('role-edit') ?? false;
$this->canDelect = Auth::user()?->can('role-delete') ?? false;
} }
public function setUp(): array public function setUp(): array
{ {
//$this->showCheckBox(); if($this->canDelect){
$this->showCheckBox();
return [ }
PowerGrid::header() $actions = [];
// ->showSearchInput(), $header =PowerGrid::header();
->includeViewOnTop('livewire.admin.role-header'), if($this->canCreate){
//PowerGrid::footer() $header->includeViewOnTop('livewire.admin.role-header');
// ->showPerPage() }
// ->showRecordCount(), $actions[]=$header;
]; $actions[]=PowerGrid::footer()
->showPerPage()
->showRecordCount();
return $actions;
} }
public function header(): array
{
$actions = [];
if ($this->canDelect) {
$actions[]=Button::add('bulk-delete')
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatch('bulkDelete.' . $this->tableName, []);
}
return $actions;
}
public function datasource(): Builder public function datasource(): Builder
{ {
@ -67,18 +90,49 @@ final class RoleTable extends PowerGridComponent
public function columns(): array public function columns(): array
{ {
return [ $column=[];
Column::make(__('roles.no'), 'id')->sortable()->searchable(), $column[]=Column::make(__('roles.no'), 'id')->sortable()->searchable();
Column::make(__('roles.name'), 'name')->sortable()->searchable(), $column[]=Column::make(__('roles.name'), 'name')->sortable()->searchable()
Column::make(__('roles.permissions'), 'permissions_list'), ->editOnClick(
Column::make('Created at', 'created_at_formatted', 'created_at')->sortable(), hasPermission: $this->canEdit,
Column::action('Action') dataField: 'name',
]; fallback: 'N/A',
saveOnMouseOut: true
);
$column[]=Column::make(__('roles.permissions'), 'permissions_list');
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable();
$column[]=Column::action('Action');
return $column;
}
#[On('bulkDelete.{tableName}')]
public function bulkDelete(): void
{
if ($this->canDelect) {
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
if($this->checkboxValues){
Role::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
}
}
}
#[On('onUpdatedEditable')]
public function onUpdatedEditable($id, $field, $value): void
{
if ($field === 'name' && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
private function noUpdated($id,$field,$value){
$role = Role::find($id);
if ($role) {
$role->{$field} = $value;
$role->save(); // 明確觸發 saving
}
} }
public function filters(): array public function filters(): array
{ {
return [ return [
Filter::inputText('name')->placeholder(__('roles.name')),
Filter::datetimepicker('created_at'), Filter::datetimepicker('created_at'),
]; ];
} }
@ -86,18 +140,22 @@ final class RoleTable extends PowerGridComponent
public function actions(Role $row): array public function actions(Role $row): array
{ {
return [ $actions = [];
Button::add('edit') if ($this->canEdit) {
$actions[] =Button::add('edit')
->slot(__('roles.edit')) ->slot(__('roles.edit'))
->icon('solid-pencil-square') ->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]), ->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]);
Button::add('delete') }
if($this->canDelect){
$actions[] =Button::add('delete')
->slot(__('roles.delete')) ->slot(__('roles.delete'))
->icon('solid-trash') ->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.role-form', 'deleteRole', ['id' => $row->id]), ->dispatchTo('admin.role-form', 'deleteRole', ['id' => $row->id]);
]; }
return $actions;
} }
/* /*

View File

@ -72,6 +72,9 @@ class SongForm extends Component
$song = Song::findOrFail($id); $song = Song::findOrFail($id);
$this->songId = $song->id; $this->songId = $song->id;
$this->fields = $song->only(array_keys($this->fields)); $this->fields = $song->only(array_keys($this->fields));
$this->selectedCategories = $song->categories()->pluck('id')->toArray();
$this->selectedArtists = $song->artists()->pluck('id')->toArray();
//dd($this->fields,$this->selectedCategories,$this->selectedArtists);
$this->showCreateModal = true; $this->showCreateModal = true;
} }
@ -84,9 +87,12 @@ class SongForm extends Component
$song->update($this->fields); $song->update($this->fields);
session()->flash('message', '歌曲已更新'); session()->flash('message', '歌曲已更新');
} else { } else {
Song::create($this->fields); $song = Song::create($this->fields);
session()->flash('message', '歌曲已新增'); session()->flash('message', '歌曲已新增');
} }
// ⭐ 同步多對多關聯
$song->artists()->sync($this->selectedArtists ?? []);
$song->categories()->sync($this->selectedCategories ?? []);
$this->resetFields(); $this->resetFields();
$this->showCreateModal = false; $this->showCreateModal = false;

View File

@ -7,6 +7,7 @@ use App\Enums\SongLanguageType;
use App\Enums\SongSituation; use App\Enums\SongSituation;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Button;
@ -23,6 +24,10 @@ use Livewire\Attributes\On;
final class SongTable extends PowerGridComponent final class SongTable extends PowerGridComponent
{ {
public string $tableName = 'song-table'; public string $tableName = 'song-table';
public bool $canCreate;
public bool $canEdit;
public bool $canDownload;
public bool $canDelect;
/* public bool $deferLoading = true; /* public bool $deferLoading = true;
@ -33,36 +38,43 @@ final class SongTable extends PowerGridComponent
public function boot(): void public function boot(): void
{ {
config(['livewire-powergrid.filter' => 'outside']); config(['livewire-powergrid.filter' => 'outside']);
$this->canCreate = Auth::user()?->can('song-edit') ?? false;
$this->canEdit = Auth::user()?->can('song-edit') ?? false;
$this->canDownload=Auth::user()?->can('song-delete') ?? false;
$this->canDelect = Auth::user()?->can('song-delete') ?? false;
} }
public function setUp(): array public function setUp(): array
{ {
$this->showCheckBox(); if($this->canDownload || $this->canDelect){
$this->showCheckBox();
}
$actions = [];
if($this->canDownload){
$actions[]=PowerGrid::exportable(fileName: 'song-file')
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
}
$header = PowerGrid::header()->showSoftDeletes()->showToggleColumns();
if($this->canCreate){
$header->includeViewOnTop('livewire.admin.song-header');
}
$actions[]=$header;
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
return [ return $actions;
PowerGrid::exportable(fileName: 'song-file')
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV),
PowerGrid::header()
->showSoftDeletes()
->showToggleColumns()
//->showSearchInput()
->includeViewOnTop('livewire.admin.song-header')
,
PowerGrid::footer()
->showPerPage()
->showRecordCount(),
];
} }
public function header(): array public function header(): array
{ {
return [ $actions = [];
Button::add('bulk-delete') if ($this->canDelect) {
$actions[]=Button::add('bulk-delete')
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)') ->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold']) ->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatch('bulkDelete.' . $this->tableName, []), ->dispatch('bulkDelete.' . $this->tableName, []);
]; }
return $actions;
} }
public function datasource(): Builder public function datasource(): Builder
@ -94,7 +106,7 @@ final class SongTable extends PowerGridComponent
return Blade::render( return Blade::render(
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>', '<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[ [
'options' => $this->languageTypeSelectOptions(), 'options' => SongLanguageType::options(),
'modelId' => intval($model->id), 'modelId' => intval($model->id),
'fieldName'=>'language_type', 'fieldName'=>'language_type',
'selected' => $model->language_type->value 'selected' => $model->language_type->value
@ -108,7 +120,7 @@ final class SongTable extends PowerGridComponent
return Blade::render( return Blade::render(
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>', '<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[ [
'options' => $this->situationSelectOptions(), 'options' => SongSituation::options(),
'modelId' => intval($model->id), 'modelId' => intval($model->id),
'fieldName'=>'situation', 'fieldName'=>'situation',
'selected' => $model->situation->value 'selected' => $model->situation->value
@ -126,87 +138,41 @@ final class SongTable extends PowerGridComponent
public function columns(): array public function columns(): array
{ {
return [ $column=[];
Column::make(__('songs.id'), 'id'), $column[]=Column::make(__('songs.id'), 'id')
Column::make(__('songs.name'), 'name') ->editOnClick(hasPermission: $this->canEdit, dataField: 'id', fallback: 'N/A', saveOnMouseOut: true);
->sortable() $column[]=Column::make(__('songs.name'), 'name')->sortable()->searchable()
->searchable(), ->editOnClick(hasPermission: $this->canEdit, dataField: 'name', fallback: 'N/A', saveOnMouseOut: true);
Column::make(__('songs.simplified'), 'simplified') $column[]=Column::make(__('songs.simplified'), 'simplified')->sortable()->searchable()->hidden(true, false);
->sortable() $column[]=Column::make(__('songs.name.phinetic'), 'phonetic_abbr')->sortable()->searchable()->hidden(true, false);
->searchable() $column[]=Column::make(__('songs.name.pinyin'), 'pinyin_abbr')->sortable()->searchable()->hidden(true, false);
->hidden(true, false), $column[]=Column::make(__('songs.name.strokes'), 'strokes_abbr')->sortable()->searchable()->hidden(true, false);
Column::make(__('songs.name.phinetic'), 'phonetic_abbr') $column[]=Column::make(__('songs.filename'), 'filename')->sortable()->searchable()
->sortable() ->editOnClick(hasPermission: $this->canEdit, dataField: 'filename', fallback: 'N/A', saveOnMouseOut: true);
->searchable() $column[]=Column::make(__('songs.adddate'), 'adddate_formatted', 'adddate')->sortable();
->hidden(true, false), $column[]=Column::make(__('songs.artists'), 'song_artists');
Column::make(__('songs.name.pinyin'), 'pinyin_abbr') $column[]=Column::make(__('songs.language_type'),'language_type_str', 'songs.language_type')->searchable();
->sortable() $column[]=Column::make(__('songs.categorys'), 'song_categories');
->searchable() $column[]=Column::make('Db change', 'db_change')
->hidden(true, false), ->editOnClick(hasPermission: $this->canEdit, dataField: 'db_change', fallback: 'N/A', saveOnMouseOut: true);
Column::make(__('songs.name.strokes'), 'strokes_abbr') $column[]=Column::make(__('songs.vocal'), 'vocal')->toggleable(hasPermission: true, trueLabel: 'yes', falseLabel: 'no');
->sortable() $column[]=Column::make(__('songs.situation'), 'situation_str','songs.situation')->searchable();
->searchable() $column[]=Column::make(__('songs.copyright01'), 'copyright01')->sortable()->searchable()
->hidden(true, false), ->editOnClick(hasPermission: $this->canEdit, dataField: 'copyright01', fallback: 'N/A', saveOnMouseOut: true);
$column[]=Column::make(__('songs.copyright02'), 'copyright02')->sortable()->searchable()->hidden(true, false)
Column::make(__('songs.filename'), 'filename') ->editOnClick(hasPermission: $this->canEdit, dataField: 'copyright02', fallback: 'N/A', saveOnMouseOut: true);
->sortable() $column[]=Column::make(__('songs.note01'), 'note01')->sortable()->searchable()
->searchable(), ->editOnClick(hasPermission: $this->canEdit, dataField: 'note01', fallback: 'N/A', saveOnMouseOut: true);
Column::make(__('songs.adddate'), 'adddate_formatted', 'adddate') $column[]=Column::make(__('songs.note02'), 'note02')->sortable()->searchable()->hidden(true, false)
->sortable(), ->editOnClick(hasPermission: $this->canEdit, dataField: 'note02', fallback: 'N/A', saveOnMouseOut: true);
//歌手 $column[]=Column::make(__('songs.note03'), 'note03')->sortable()->searchable()->hidden(true, false)
Column::make(__('songs.artists'), 'song_artists'), ->editOnClick(hasPermission: $this->canEdit, dataField: 'note03', fallback: 'N/A', saveOnMouseOut: true);
Column::make(__('songs.language_type'),'language_type_str', 'songs.language_type')->searchable(), $column[]=Column::make(__('songs.note04'), 'note04')->sortable()->searchable()->hidden(true, false)
//分類 ->editOnClick(hasPermission: $this->canEdit, dataField: 'note04', fallback: 'N/A', saveOnMouseOut: true);
Column::make(__('songs.categorys'), 'song_categories'), $column[]=Column::make(__('songs.enable'), 'enable')->toggleable(hasPermission: true, trueLabel: 'yes', falseLabel: 'no');
//點播次數 $column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
Column::make('Db change', 'db_change'), $column[]=Column::action(__('songs.actions'));
Column::make(__('songs.vocal'), 'vocal') return $column;
->toggleable(hasPermission: true, trueLabel: 'yes', falseLabel: 'no'),
Column::make(__('songs.situation'), 'situation_str','songs.situation')->searchable(),
Column::make(__('songs.copyright01'), 'copyright01')
->sortable()
->searchable()
->editOnClick(hasPermission: true, dataField: 'copyright01', fallback: 'N/A', saveOnMouseOut: true),
Column::make(__('songs.copyright02'), 'copyright02')
->sortable()
->searchable()
->editOnClick(hasPermission: true, dataField: 'copyright02', fallback: 'N/A', saveOnMouseOut: true)
->hidden(true, false),
Column::make(__('songs.note01'), 'note01')
->sortable()
->searchable()
->editOnClick(hasPermission: true, dataField: 'note01', fallback: 'N/A', saveOnMouseOut: true),
Column::make(__('songs.note02'), 'note02')
->sortable()
->searchable()
->editOnClick(hasPermission: true, dataField: 'note02', fallback: 'N/A', saveOnMouseOut: true)
->hidden(true, false),
Column::make(__('songs.note03'), 'note03')
->sortable()
->searchable()
->editOnClick(hasPermission: true, dataField: 'note03', fallback: 'N/A', saveOnMouseOut: true)
->hidden(true, false),
Column::make(__('songs.note04'), 'note04')
->sortable()
->searchable()
->editOnClick(hasPermission: true, dataField: 'note04', fallback: 'N/A', saveOnMouseOut: true)
->hidden(true, false),
Column::make(__('songs.enable'), 'enable')
->toggleable(hasPermission: true, trueLabel: 'yes', falseLabel: 'no'),
Column::make('Created at', 'created_at_formatted', 'created_at')
->sortable()
->hidden(true, false),
Column::action(__('songs.actions'))
];
} }
public function filters(): array public function filters(): array
@ -232,58 +198,70 @@ final class SongTable extends PowerGridComponent
Filter::datetimepicker('created_at'), Filter::datetimepicker('created_at'),
]; ];
} }
public function languageTypeSelectOptions():Collection
{
return collect(SongLanguageType::cases())->mapWithKeys(function (SongLanguageType $case) {
return [$case->value => $case->labels()];
});
}
public function situationSelectOptions():Collection
{
return collect(SongSituation::cases())->mapWithKeys(function (SongSituation $case) {
return [$case->value => $case->labels()];
});
}
#[On('categoryChanged')]
public function categoryChanged($value,$fieldName, $modelId): void
{
// dd($value,$fieldName, $modelId);
if (in_array($fieldName, ['language_type', 'situation'])) {
Artist::find($modelId)?->update([$fieldName => $value]);
}
}
#[On('bulkDelete.{tableName}')] #[On('bulkDelete.{tableName}')]
public function bulkDelete(): void public function bulkDelete(): void
{ {
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))'); $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
if($this->checkboxValues){ if($this->checkboxValues){
Artist::destroy($this->checkboxValues); Song::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface. $this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
} }
} }
#[On('categoryChanged')]
public function categoryChanged($value,$fieldName, $modelId): void
{
// dd($value,$fieldName, $modelId);
if (in_array($fieldName, ['language_type', 'situation'])) {
$this->noUpdated($modelId,$fieldName,$value);
}
}
#[On('onUpdatedEditable')]
public function onUpdatedEditable($id, $field, $value): void
{
if (in_array($field,[
'name','filename','db_change',
'copyright01','copyright02','note01','note02','note03','note04'
]) && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
#[On('onUpdatedToggleable')]
public function onUpdatedToggleable($id, $field, $value): void
{
if (in_array($field,['vocal','enable']) && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
private function noUpdated($id,$field,$value){
$song = Song::find($id);
if ($song) {
$song->{$field} = $value;
$song->save(); // 明確觸發 saving
}
}
public function actions(Song $row): array public function actions(Song $row): array
{ {
return [ $actions = [];
if ($this->canEdit) {
Button::add('edit') $actions[]=Button::add('edit')
->slot(__('songs.edit')) ->slot(__('songs.edit'))
->icon('solid-pencil-square') ->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.song-form', 'openEditSongModal', ['id' => $row->id]), ->dispatchTo('admin.song-form', 'openEditSongModal', ['id' => $row->id]);
Button::add('delete') }
if($this->canDelect){
$actions[]=Button::add('delete')
->slot(__('songs.delete')) ->slot(__('songs.delete'))
->icon('solid-trash') ->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.song-form', 'deleteSong', ['id' => $row->id]), ->dispatchTo('admin.song-form', 'deleteSong', ['id' => $row->id]);
]; }
}
public function onUpdatedToggleable($id, $field, $value): void return $actions;
{
$updated = Song::query()->where('id', $id)->update([
$field => $value,
]);
} }
/* /*
public function actionRules($row): array public function actionRules($row): array
{ {

View File

@ -6,6 +6,8 @@ use App\Models\User;
use App\Enums\UserGender; use App\Enums\UserGender;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button; use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column; use PowerComponents\LivewirePowerGrid\Column;
@ -25,38 +27,50 @@ final class UserTable extends PowerGridComponent
public string $tableName = 'user-table'; public string $tableName = 'user-table';
public bool $showFilters = false; public bool $showFilters = false;
public bool $canCreate;
public bool $canEdit;
public bool $canDownload;
public bool $canDelect;
public function boot(): void public function boot(): void
{ {
config(['livewire-powergrid.filter' => 'outside']); config(['livewire-powergrid.filter' => 'outside']);
//權限設定
$this->canCreate = Auth::user()?->can('user-edit') ?? false;
$this->canEdit = Auth::user()?->can('user-edit') ?? false;
$this->canDownload=Auth::user()?->can('user-delete') ?? false;
$this->canDelect = Auth::user()?->can('user-delete') ?? false;
} }
public function setUp(): array public function setUp(): array
{ {
$this->showCheckBox(); if($this->canDownload || $this->canDelect){
$this->showCheckBox();
return [ }
PowerGrid::exportable(fileName: 'my-export-file') $actions = [];
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV), $actions[] =PowerGrid::exportable(fileName: 'user-file')
PowerGrid::header() ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
//->showSoftDeletes() $header = PowerGrid::header()
->showToggleColumns() ->showToggleColumns();
//->showSearchInput() if($this->canCreate){
->includeViewOnTop('livewire.admin.user-header') $header->includeViewOnTop('livewire.admin.user-header');
, }
PowerGrid::footer()->showPerPage()->showRecordCount(), $actions[]=$header;
]; $actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
return $actions;
} }
public function header(): array public function header(): array
{ {
return [ $actions = [];
Button::add('bulk-delete') if ($this->canDelect) {
$actions[]=Button::add('bulk-delete')
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)') ->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold']) ->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatch('bulkDelete.' . $this->tableName, []), ->dispatch('bulkDelete.' . $this->tableName, []);
]; }
return $actions;
} }
public function datasource(): Builder public function datasource(): Builder
@ -71,14 +85,45 @@ final class UserTable extends PowerGridComponent
public function fields(): PowerGridFields public function fields(): PowerGridFields
{ {
return PowerGrid::fields() return PowerGrid::fields()
->add('id') ->add('id')
->add('name') ->add('name')
->add('email') ->add('email')
->add('phone') ->add('phone')
->add('birthday_formatted',fn (User $model) => Carbon::parse($model->birthday)->format('Y-m-d')) ->add('birthday_formatted',fn (User $model) => Carbon::parse($model->birthday)->format('Y-m-d'))
->add('gender', fn (User $model) => UserGender::from($model->gender)->labels()) ->add('gender_str', function (User $model) {
->add('status', fn (User $model) => UserStatus::from($model->status)->labels()) if ($this->canEdit) {
return Blade::render(
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[
'options' => UserGender::options(),
'modelId' => intval($model->id),
'fieldName'=>'gender',
'selected' => $model->gender->value
]
);
}
// 沒有權限就顯示對應的文字
return $model->gender->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
} )
->add('status_str', function (User $model) {
if ($this->canEdit) {
return Blade::render(
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[
'options' => UserStatus::options(),
'modelId' => intval($model->id),
'fieldName'=>'status',
'selected' => $model->status->value
]
);
}
// 沒有權限就顯示對應的文字
return $model->status->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
} )
->add('roles' ,fn(User $model)=> $model->roles->pluck('name')->implode(', ')) ->add('roles' ,fn(User $model)=> $model->roles->pluck('name')->implode(', '))
->add('created_at_formatted', fn (User $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s')); ->add('created_at_formatted', fn (User $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
} }
@ -91,7 +136,7 @@ final class UserTable extends PowerGridComponent
->sortable() ->sortable()
->searchable() ->searchable()
->editOnClick( ->editOnClick(
hasPermission: true, hasPermission: $this->canEdit,
dataField: 'name', dataField: 'name',
fallback: 'N/A', fallback: 'N/A',
saveOnMouseOut: true saveOnMouseOut: true
@ -100,7 +145,7 @@ final class UserTable extends PowerGridComponent
->sortable() ->sortable()
->searchable() ->searchable()
->editOnClick( ->editOnClick(
hasPermission: true, hasPermission: $this->canEdit,
dataField: 'email', dataField: 'email',
fallback: 'N/A', fallback: 'N/A',
saveOnMouseOut: true saveOnMouseOut: true
@ -109,32 +154,71 @@ final class UserTable extends PowerGridComponent
->sortable() ->sortable()
->searchable() ->searchable()
->editOnClick( ->editOnClick(
hasPermission: true, hasPermission: $this->canEdit,
dataField: 'phone', dataField: 'phone',
fallback: 'N/A', fallback: 'N/A',
saveOnMouseOut: true saveOnMouseOut: true
), ),
Column::make(__('users.gender'), 'gender','users.gender'), Column::make(__('users.gender'), 'gender_str','users.gender'),
Column::make(__('users.birthday'), 'birthday_formatted')->sortable()->searchable(), Column::make(__('users.birthday'), 'birthday_formatted')->sortable()->searchable(),
Column::make(__('users.status'), 'status','users.status'), Column::make(__('users.status'), 'status_str','users.status'),
Column::make(__('users.role'), 'roles'), Column::make(__('users.role'), 'roles'),
Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(), Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(),
Column::action('操作') Column::action('操作')
]; ];
} }
#[On('bulkDelete.{tableName}')]
public function bulkDelete(): void
{
if ($this->canDelect) {
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
if($this->checkboxValues){
Artist::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
}
}
}
#[On('categoryChanged')]
public function categoryChanged($value,$fieldName, $modelId): void
{
// dd($value,$fieldName, $modelId);
if (in_array($fieldName,['gender','status']) && $this->canEdit) {
$this->noUpdated($modelId,$fieldName,$value);
}
}
#[On('onUpdatedEditable')]
public function onUpdatedEditable($id, $field, $value): void
{
if (in_array($field,['name','email','phone']) && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
#[On('onUpdatedToggleable')]
public function onUpdatedToggleable($id, $field, $value): void
{
if (in_array($field,[]) && $this->canEdit) {
$this->noUpdated($id,$field,$value);
}
}
private function noUpdated($id,$field,$value){
$artist = Artist::find($id);
if ($artist) {
$artist->{$field} = $value;
$artist->save(); // 明確觸發 saving
}
}
public function filters(): array public function filters(): array
{ {
return [ return [
Filter::inputText('name')->placeholder(__('users.name')), Filter::inputText('name')->placeholder(__('users.name')),
Filter::inputText('email')->placeholder('Email'), Filter::inputText('email')->placeholder('Email'),
Filter::inputText('phone')->placeholder(__('users.phone')), Filter::inputText('phone')->placeholder(__('users.phone')),
Filter::enumSelect('gender','users.gender') Filter::enumSelect('gender_str','users.gender')
->datasource(UserGender::cases()) ->datasource(UserGender::cases())
->optionLabel('users.gender'), ->optionLabel('users.gender'),
Filter::datepicker('birthday'), Filter::datepicker('birthday'),
Filter::enumSelect('status', 'users.status') Filter::enumSelect('status_str', 'users.status')
->datasource(UserStatus::cases()) ->datasource(UserStatus::cases())
->optionLabel('users.status'), ->optionLabel('users.status'),
Filter::datetimepicker('created_at'), Filter::datetimepicker('created_at'),
@ -143,47 +227,22 @@ final class UserTable extends PowerGridComponent
public function actions(User $row): array public function actions(User $row): array
{ {
return [ $actions = [];
if ($this->canEdit) {
Button::add('edit') $actions[]=Button::add('edit')
->slot(__('users.edit')) ->slot(__('users.edit'))
->icon('solid-pencil-square') ->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.user-form', 'openEditUserModal', ['id' => $row->id]), ->dispatchTo('admin.user-form', 'openEditUserModal', ['id' => $row->id]);
Button::add('delete') }
if($this->canDelect){
$actions[]=Button::add('delete')
->slot(__('users.delete')) ->slot(__('users.delete'))
->icon('solid-trash') ->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ') ->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.user-form', 'deleteUser', ['id' => $row->id]), ->dispatchTo('admin.user-form', 'deleteUser', ['id' => $row->id]);
];
}
public function onUpdatedEditable($id, $field, $value): void
{
$updated = User::query()->where('id', $id)->update([
$field => $value,
]);
if ($updated) {
$this->fillData();
}
}
public function onUpdatedToggleable($id, $field, $value): void
{
$updated = User::query()->where('id', $id)->update([
$field => $value,
]);
if ($updated) {
$this->fillData();
}
}
#[On('bulkDelete.{tableName}')]
public function bulkDelete(): void
{
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
if($this->checkboxValues){
User::destroy($this->checkboxValues);
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
} }
return $actions;
} }

View File

@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Helpers\ChineseNameConverter; use App\Helpers\ChineseNameConverter;
use App\Helpers\ChineseStrokesConverter;
class Song extends Model class Song extends Model
{ {
@ -70,7 +71,7 @@ class Song extends Model
$chars = preg_split('//u', $song->name, -1, PREG_SPLIT_NO_EMPTY); $chars = preg_split('//u', $song->name, -1, PREG_SPLIT_NO_EMPTY);
$firstChar = $chars[0] ?? null; $firstChar = $chars[0] ?? null;
$song->strokes_abbr=$firstChar ? getStrokeCountFromChar::getStrokes($firstChar) : null; $song->strokes_abbr=$firstChar ? ChineseStrokesConverter::getStrokes($firstChar) : null;
}); });
} }
} }

View File

@ -48,7 +48,9 @@ class User extends Authenticatable
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'birthday' => 'date' 'birthday' => 'date',
'gender' => \App\Enums\UserGender::class,
'status' => \App\Enums\UserStatus::class,
]; ];
} }

View File

@ -6,6 +6,7 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
api: __DIR__.'/../routes/api.php',
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',

View File

@ -30,7 +30,8 @@ return new class extends Migration
$table->string('simplified')->comment('歌曲簡體'); $table->string('simplified')->comment('歌曲簡體');
$table->string('phonetic_abbr')->comment('歌曲注音'); $table->string('phonetic_abbr')->comment('歌曲注音');
$table->string('pinyin_abbr')->comment('歌曲拼音'); $table->string('pinyin_abbr')->comment('歌曲拼音');
$table->integer('strokes_abbr')->nullable()->comment('歌曲筆劃'); $table->integer('strokes_abbr')->default(0)->comment('歌曲筆劃');
$table->integer('song_counts')->default(0)->comment('點播次數');
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -16,9 +16,9 @@ class ChineseNameConverterSeeder extends Seeder
{ {
//dd(ChineseStrokesConverter::getStrokes('羅')); //dd(ChineseStrokesConverter::getStrokes('羅'));
echo "羅:" . ChineseStrokesConverter::getStrokes('羅') . PHP_EOL; //echo "羅:" . ChineseStrokesConverter::getStrokes('羅') . PHP_EOL;
echo "陳:" . ChineseStrokesConverter::getStrokes('陳') . PHP_EOL; //echo "陳:" . ChineseStrokesConverter::getStrokes('陳') . PHP_EOL;
echo "阪:" . ChineseStrokesConverter::getStrokes('阪') . PHP_EOL; //echo "阪:" . ChineseStrokesConverter::getStrokes('阪') . PHP_EOL;
//dd(ChineseNameConverter::convertAll('比愛更愛')); dd(ChineseNameConverter::convertAll('小嗯'));
} }
} }

View File

@ -4,6 +4,7 @@ return [
'management' => '歌手管理', 'management' => '歌手管理',
'list' => '歌手列表', 'list' => '歌手列表',
'CreateNew' => '新增歌手', 'CreateNew' => '新增歌手',
'EditArtist' => '編輯歌手',
'create_edit' => '新增 / 編輯', 'create_edit' => '新增 / 編輯',
'create' => '新增', 'create' => '新增',
'edit' => '編輯', 'edit' => '編輯',
@ -15,6 +16,8 @@ return [
'name.phinetic' => '注音', 'name.phinetic' => '注音',
'name.pinyin' => '拼音', 'name.pinyin' => '拼音',
'name.strokes' => '筆劃', 'name.strokes' => '筆劃',
'select_category' =>'選擇類別',
'actions' => '操作', 'actions' => '操作',
'view' => '查看', 'view' => '查看',

View File

@ -3,12 +3,16 @@
return [ return [
'list' => '角色列表', 'list' => '角色列表',
'CreateNew' => '新增角色', 'CreateNew' => '新增角色',
'EditRole' => '編輯角色',
'edit' => '編輯', 'edit' => '編輯',
'delete' => '刪除', 'delete' => '刪除',
'no' => '編號', 'no' => '編號',
'name' => '名稱', 'name' => '名稱',
'permissions' => '權限', 'permissions' => '權限',
'role_name' =>'角色名稱',
'select_permissions'=>'選擇權限',
'create' => '新增', 'create' => '新增',
'action' => '操作', 'action' => '操作',

View File

@ -4,6 +4,7 @@ return [
'management' => '歌曲管理', 'management' => '歌曲管理',
'list' => '歌曲列表', 'list' => '歌曲列表',
'CreateNew' => '新增歌曲', 'CreateNew' => '新增歌曲',
'EditSong' => '編輯歌曲',
'create_edit' => '新增 / 編輯', 'create_edit' => '新增 / 編輯',
'create' => '新增', 'create' => '新增',
'edit' => '編輯', 'edit' => '編輯',
@ -27,10 +28,17 @@ return [
'note04' => '備註04', 'note04' => '備註04',
'enable' => '狀態', 'enable' => '狀態',
'name_length' => '歌名字數', 'name_length' => '歌名字數',
'db_change'=>'分貝增減',
'vocal' => '人聲', 'vocal' => '人聲',
'situation' => '情境', 'situation' => '情境',
'simplified' => '歌名簡體', 'simplified' => '歌名簡體',
'select_artists' =>'輸入搜尋歌手',
'select_language_type'=>'選擇語言',
'select_categorys'=>'選擇分類',
'select_situation'=>'選擇情境',
'actions' => '操作', 'actions' => '操作',
'view' => '查看', 'view' => '查看',
'submit' => '提交', 'submit' => '提交',

View File

@ -3,6 +3,7 @@
return [ return [
'list' => '使用者列表', 'list' => '使用者列表',
'CreateNew' => '新增使用者', 'CreateNew' => '新增使用者',
'EditUser' => '編輯使用者',
'edit' => '編輯', 'edit' => '編輯',
'delete' => '刪除', 'delete' => '刪除',
@ -13,6 +14,10 @@ return [
'birthday' => '生日', 'birthday' => '生日',
'status' => '狀態', 'status' => '狀態',
'role' =>'角色', 'role' =>'角色',
'select_gender'=>'選擇性別',
'select_status'=>'選擇狀態',
'select_role'=>'選擇角色',
'create' => '新增', 'create' => '新增',
'action' => '操作', 'action' => '操作',

View File

@ -1,9 +1,9 @@
@if(auth()->user()->hasRole('Admin')) @if(auth()->user()->hasRole('User'))
<x-layouts.admin>
{{ $slot }}
</x-layouts.admin>
@else
<x-layouts.user> <x-layouts.user>
{{ $slot }} {{ $slot }}
</x-layouts.user> </x-layouts.user>
@else
<x-layouts.admin>
{{ $slot }}
</x-layouts.admin>
@endif @endif

View File

@ -1,20 +1,20 @@
<x-wireui:modal-card title="{{ $artistId ? '編輯歌手' : '新增歌手' }}" blur wire:model.defer="showCreateModal"> <x-wireui:modal-card title="{{ $artistId ? __('artists.EditArtist') : __('artists.CreateNew') }}" blur wire:model.defer="showCreateModal">
<div class="space-y-4"> <div class="space-y-4">
<x-wireui:select <x-wireui:select
label="類別" label="{{__('artists.category')}}"
wire:model.defer="fields.category" wire:model.defer="fields.category"
placeholder="選擇類別" placeholder="{{__('artists.select_category')}}"
:options="$categoryOptions" :options="$categoryOptions"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:input label="名稱" wire:model.defer="fields.name" /> <x-wireui:input label="{{__('artists.name')}}" wire:model.defer="fields.name" />
</div> </div>
<x-slot name="footer"> <x-slot name="footer">
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<x-wireui:button flat label="{{__('atrists.cancel')}}" @click="$wire.showCreateModal = false" /> <x-wireui:button flat label="{{__('artists.cancel')}}" @click="$wire.showCreateModal = false" />
<x-wireui:button primary type="submit" label="儲存" /> <x-wireui:button primary label="{{__('artists.submit')}}" wire:click="save" />
</div> </div>
</x-slot> </x-slot>
</x-wireui:modal-card> </x-wireui:modal-card>

View File

@ -1,25 +1,21 @@
<div> <x-wireui:modal-card title="{{ $roleId ? __('roles.EditRole') : __('roles.CreateNew') }}" blur wire:model.defer="showCreateModal">
@if ($showCreateModal) <div class="space-y-4">
<x-wireui:modal-card title="{{ $roleId ? '編輯角色' : '新增角色' }}" blur wire:model.defer="showCreateModal"> <x-wireui:input label="{{__('roles.role_name')}}" wire:model.defer="name" />
<div class="space-y-4"> <x-wireui:select
<x-wireui:input label="角色名稱" wire:model.defer="name" /> label="{{__('roles.permissions')}}"
<x-wireui:select wire:model.defer="selectedPermissions"
label="權限" placeholder="{{__('roles.select_permissions')}}"
wire:model.defer="selectedPermissions" multiselect
placeholder="選擇權限" option-label="label"
multiselect option-value="value"
option-label="label" :options="$permissions->map(fn($p) => ['value' => $p->id, 'label' => $p->name])->toArray()"
option-value="value" />
:options="$permissions->map(fn($p) => ['value' => $p->id, 'label' => $p->name])->toArray()" </div>
/>
</div>
<x-slot name="footer"> <x-slot name="footer">
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<x-wireui:button flat label="{{__('roles.cancel')}}" @click="$wire.showCreateModal = false" /> <x-wireui:button flat label="{{__('roles.cancel')}}" @click="$wire.showCreateModal = false" />
<x-wireui:button primary label="{{__('roles.submit')}}" wire:click="save" /> <x-wireui:button primary label="{{__('roles.submit')}}" wire:click="save" />
</div> </div>
</x-slot> </x-slot>
</x-wireui:modal-card> </x-wireui:modal-card>
@endif
</div>

View File

@ -1,17 +1,15 @@
<x-wireui:modal-card title="{{ $songId ? '編輯歌曲' : '新增歌曲' }}" wire:model.defer="showCreateModal"> <x-wireui:modal-card title="{{ $songId ? __('songs.EditSong') : __('songs.CreateNew') }}" wire:model.defer="showCreateModal">
<form wire:submit.prevent="save">
<div class="grid grid-cols-3 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-3 gap-4 sm:grid-cols-3">
<x-wireui:input label="歌曲編號" wire:model.defer="fields.id" required /> <x-wireui:input label="{{__('songs.id')}}" wire:model.defer="fields.id" required />
<x-wireui:input label="歌曲名稱" wire:model.defer="fields.name" required /> <x-wireui:input label="{{__('songs.name')}}" wire:model.defer="fields.name" required />
<x-wireui:input label="檔名" wire:model.defer="fields.filename" /> <x-wireui:input label="{{__('songs.filename')}}" wire:model.defer="fields.filename" />
</div> </div>
<div class="grid grid-cols-3 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-3 gap-4 sm:grid-cols-3">
<x-wireui:select <x-wireui:select
label="歌手" label="{{__('songs.artists')}}"
wire:model.defer="selectedArtists" wire:model.defer="selectedArtists"
placeholder="輸入搜尋歌手" placeholder="{{__('songs.select_artists')}}"
:async-data="route('api.artists.search')" :async-data="route('api.artists.search')"
option-label="name" option-label="name"
option-value="id" option-value="id"
@ -19,56 +17,56 @@
hide-empty-message hide-empty-message
/> />
<x-wireui:select <x-wireui:select
label="語言類型" label="{{__('songs.language_type')}}"
wire:model.defer="fields.language_type" wire:model.defer="fields.language_type"
placeholder="選擇語言" placeholder="{{__('songs.select_language_type')}}"
:options="$songLanguageType" :options="$songLanguageType"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:select <x-wireui:select
label="分類" label="{{__('songs.categorys')}}"
wire:model.defer="selectedCategories" wire:model.defer="selectedCategories"
:options="$songCategories->map(fn($c) => ['label' => $c->name, 'value' => $c->id])->toArray()" :options="$songCategories->map(fn($c) => ['label' => $c->name, 'value' => $c->id])->toArray()"
option-label="label" option-label="label"
option-value="value" option-value="value"
multiselect multiselect
placeholder="選擇分類" placeholder="{{__('songs.select_categorys')}}"
/> />
</div> </div>
<div class="grid grid-cols-3 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-3 gap-4 sm:grid-cols-3">
<x-wireui:select <x-wireui:select
label="情境" label="{{__('songs.situation')}}"
wire:model.defer="fields.situation" wire:model.defer="fields.situation"
placeholder="選擇情境" placeholder="{{__('songs.select_situation')}}"
:options="$songSituation" :options="$songSituation"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:input label="分貝增減" wire:model.defer="fields.db_change" /> <x-wireui:input label="{{__('songs.db_change')}}" wire:model.defer="fields.db_change" />
<x-wireui:input label="新增日期" wire:model.defer="fields.adddate" type="date" /> <x-wireui:input label="{{__('songs.adddate')}}" wire:model.defer="fields.adddate" type="date" />
</div> </div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-2 gap-4 sm:grid-cols-2">
<x-wireui:toggle label="人聲" wire:model.defer="fields.vocal" /> <x-wireui:toggle label="{{__('songs.vocal')}}" wire:model.defer="fields.vocal" />
<x-wireui:toggle label="啟用" wire:model.defer="fields.enable" /> <x-wireui:toggle label="{{__('songs.enable')}}" wire:model.defer="fields.enable" />
</div> </div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-2 gap-4 sm:grid-cols-2">
<x-wireui:input label="版權1" wire:model.defer="fields.copyright01" /> <x-wireui:input label="{{__('songs.copyright01')}}" wire:model.defer="fields.copyright01" />
<x-wireui:input label="版權2" wire:model.defer="fields.copyright02" /> <x-wireui:input label="{{__('songs.copyright02')}}" wire:model.defer="fields.copyright02" />
</div> </div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4"> <div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
<x-wireui:textarea label="備註1" wire:model.defer="fields.note01" /> <x-wireui:textarea label="{{__('songs.note01')}}" wire:model.defer="fields.note01" />
<x-wireui:textarea label="備註2" wire:model.defer="fields.note02" /> <x-wireui:textarea label="{{__('songs.note02')}}" wire:model.defer="fields.note02" />
<x-wireui:textarea label="備註3" wire:model.defer="fields.note03" /> <x-wireui:textarea label="{{__('songs.note03')}}" wire:model.defer="fields.note03" />
<x-wireui:textarea label="備註4" wire:model.defer="fields.note04" /> <x-wireui:textarea label="{{__('songs.note04')}}" wire:model.defer="fields.note04" />
</div> </div>
<x-slot name="footer"> <x-slot name="footer">
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<x-wireui:button flat label="{{__('songs.cancel')}}" @click="$wire.showCreateModal = false" /> <x-wireui:button flat label="{{__('songs.cancel')}}" @click="$wire.showCreateModal = false" />
<x-wireui:button primary type="submit" label="儲存" /> <x-wireui:button primary label="{{__('songs.submit')}}" wire:click="save" />
</div> </div>
</x-slot> </x-slot>

View File

@ -1,29 +1,29 @@
<x-wireui:modal-card title="{{ $userId ? '編輯使用者' : '新增使用者' }}" blur wire:model.defer="showCreateModal"> <x-wireui:modal-card title="{{ $userId ? __('users.EditUser') : __('users.CreateNew') }}" blur wire:model.defer="showCreateModal">
<div class="space-y-4"> <div class="space-y-4">
<x-wireui:input label="名稱" wire:model.defer="fields.name" required /> <x-wireui:input label="{{__('users.name')}}" wire:model.defer="fields.name" required />
<x-wireui:input label="Email" wire:model.defer="fields.email" required /> <x-wireui:input label="Email" wire:model.defer="fields.email" required />
<x-wireui:input label="Phone" wire:model.defer="fields.phone" /> <x-wireui:input label="Phone" wire:model.defer="fields.phone" />
<x-wireui:select <x-wireui:select
label="性別" label="{{__('users.gender')}}"
wire:model.defer="fields.gender" wire:model.defer="fields.gender"
placeholder="選擇性別" placeholder="{{__('users.select_gender')}}"
:options="$genderOptions" :options="$genderOptions"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:select <x-wireui:select
label="狀態" label="{{__('users.status')}}"
wire:model.defer="fields.status" wire:model.defer="fields.status"
placeholder="選擇狀態" placeholder="{{__('users.select_status')}}"
:options="$statusOptions" :options="$statusOptions"
option-label="name" option-label="name"
option-value="value" option-value="value"
/> />
<x-wireui:select <x-wireui:select
label="角色" label="{{__('users.role')}}"
wire:model.defer="selectedRoles" wire:model.defer="selectedRoles"
placeholder="選擇角色" placeholder="{{__('users.select_role')}}"
multiselect multiselect
option-label="label" option-label="label"
option-value="value" option-value="value"

View File

@ -22,10 +22,10 @@ new #[Layout('layouts.guest')] class extends Component
//$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); //$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
$user = auth()->user(); $user = auth()->user();
if ($user->hasRole('Admin')) { if ($user->hasRole('User')) {
$this->redirect(route('admin.dashboard'), navigate: true);
} else {
$this->redirect(route('dashboard'), navigate: true); $this->redirect(route('dashboard'), navigate: true);
} else {
$this->redirect(route('admin.dashboard'), navigate: true);
} }
} }
}; ?> }; ?>

7
routes/api.php Normal file
View File

@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ArtistController;
Route::get('/artists/search', [App\Http\Controllers\ArtistController::class, 'search'])->name('api.artists.search');

View File

@ -3,13 +3,11 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Livewire\Admin\Dashboard as AdminDashboard; use App\Livewire\Admin\Dashboard as AdminDashboard;
use App\Livewire\Admin\RoleTable;
use App\Livewire\Admin\UserTable;
use App\Livewire\Admin\ArtistTable;
use App\Livewire\Admin\SongTable;
Route::view('/', 'welcome'); Route::view('/', 'welcome');
Route::view('dashboard', 'dashboard') Route::view('dashboard', 'dashboard')
->middleware(['auth', 'verified']) ->middleware(['auth', 'verified'])
->name('dashboard'); ->name('dashboard');
@ -19,36 +17,16 @@ Route::view('profile', 'profile')
->name('profile'); ->name('profile');
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';
Route::get('/api/artists/search', function (Request $request) {
return \App\Models\Artist::query()
->where('name', 'like', "%{$request->input('search')}%")
->limit(20)
->get(['id', 'name']);
})->name('api.artists.search');
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () { Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', AdminDashboard::class)->name('dashboard'); Route::get('/dashboard', AdminDashboard::class)->name('dashboard');
Route::get('/roles', function () { Route::get('/roles', function () {return view('livewire.admin.roles');})->name('roles');
return view('livewire.admin.roles'); Route::get('/users', function () {return view('livewire.admin.users');})->name('users');
})->name('roles'); Route::get('/artists', function () {return view('livewire.admin.artists');})->name('artists');
Route::get('/roles-table', RoleTable::class)->name('roles-table'); Route::get('/songs', function () {return view('livewire.admin.songs');})->name('songs');
Route::get('/branches', function () {return view('livewire.admin.branches');})->name('branches');
Route::get('/users', function () { Route::get('/rooms', function () {return view('livewire.admin.rooms');})->name('rooms');
return view('livewire.admin.users');
})->name('users');
Route::get('/users-table', UserTable::class)->name('users-table');
Route::get('/artists', function () {
return view('livewire.admin.artists');
})->name('artists');
Route::get('/artists-table', ArtistTable::class)->name('artists-table');
Route::get('/songs', function () {
return view('livewire.admin.songs');
})->name('songs');
Route::get('/songs-table', ArtistTable::class)->name('songs-table');
}); });