歌手加入滙入功能

歌手新增修改頁補權限
20250505
This commit is contained in:
allen.yan 2025-05-05 14:55:20 +08:00
parent d9924bf05b
commit ac8a4f2eb0
10 changed files with 591 additions and 28 deletions

View File

@ -0,0 +1,47 @@
<?php
namespace App\Imports;
use App\Models\Artist;
use App\Enums\ArtistCategory;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Imports\HeadingRowFormatter;
class ArtistDataImport implements ToModel, WithHeadingRow
{
public int $successCount = 0;
public int $failCount = 0;
public function __construct()
{
// 關閉 heading row 格式化
HeadingRowFormatter::default('none');
}
public function model(array $row)
{
$name = trim($row['名稱'] ?? '');
$category = trim($row['類別'] ?? '未定義');
if (empty($name)) {
$this->failCount++;
return null;
}
try {
Artist::firstOrCreate(
['name' => $name],
['category' => ArtistCategory::tryFrom($category) ?? ArtistCategory::Unset]
);
$this->successCount++;
} catch (\Throwable $e) {
$this->failCount++;
}
return null;
}
public function headingRow(): int
{
return 1;
}
}

View File

@ -4,14 +4,19 @@ namespace App\Livewire\Admin;
use Livewire\Component;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use App\Models\Artist;
use App\Enums\ArtistCategory;
class ArtistForm extends Component
{
protected $listeners = ['openCreateArtistModal','openEditArtistModal', 'deleteArtist'];
protected $listeners = ['openModal','closeModal', 'deleteArtist'];
public bool $showCreateModal = false;
public bool $canCreate;
public bool $canEdit;
public bool $canDelect;
public bool $showModal = false;
public ?int $artistId = null;
public array $categoryOptions =[];
@ -29,43 +34,64 @@ class ArtistForm extends Component
'name' => $category->labels(),
'value' => $category->value,
])->toArray();
$this->canCreate = Auth::user()?->can('song-edit') ?? false;
$this->canEdit = Auth::user()?->can('song-edit') ?? false;
$this->canDelect = Auth::user()?->can('song-delete') ?? false;
}
public function openCreateArtistModal()
public function openModal($id = null)
{
$this->resetFields();
$this->showCreateModal = true;
if ($id) {
$artist = Artist::findOrFail($id);
$this->artistId = $artist->id;
$this->fields = $artist->only(array_keys($this->fields));
}
$this->showModal = true;
}
public function openEditArtistModal($id)
public function closeModal()
{
$artist = Artist::findOrFail($id);
$this->artistId = $artist->id;
$this->fields = $artist->only(array_keys($this->fields));
$this->showCreateModal = true;
$this->resetFields();
$this->showModal = false;
}
public function save()
{
if ($this->artistId) {
$artist = Artist::findOrFail($this->artistId);
$artist->update($this->fields);
session()->flash('message', '歌手已更新');
if ($this->canEdit) {
$artist = Artist::findOrFail($this->artistId);
$artist->update($this->fields);
$this->dispatch('notify', [
'title' => '成功',
'description' => '歌手已更新',
'icon' => 'success',
]);
}
} else {
$artist = Artist::create($this->fields);
session()->flash('message', '歌手已新增');
if ($canCreate) {
$artist = Artist::create($this->fields);
$this->dispatch('notify', [
'title' => '成功',
'description' => '歌手已新增',
'icon' => 'success',
]);
}
}
$this->resetFields();
$this->showCreateModal = false;
$this->showModal = false;
$this->dispatch('pg:eventRefresh-artist-table');
}
public function deleteArtist($id)
{
Artist::findOrFail($id)->delete();
session()->flash('message', '歌手已刪除');
if ($this->canDelect) {
Artist::findOrFail($id)->delete();
session()->flash('message', '歌手已刪除');
$this->dispatch('pg:eventRefresh-artist-table');
}
}
public function resetFields()

View File

@ -0,0 +1,59 @@
<?php
namespace App\Livewire\Admin;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Livewire\WithFileUploads;
use Maatwebsite\Excel\Facades\Excel;
use App\Imports\ArtistDataImport;
class ArtistImportData extends Component
{
use WithFileUploads;
protected $listeners = ['openModal','closeModal'];
public bool $canCreate;
public bool $showModal = false;
public $file;
public function mount()
{
$this->canCreate = Auth::user()?->can('song-edit') ?? false;
}
public function openModal()
{
$this->showModal = true;
}
public function closeModal()
{
$this->showModal = false;
}
public function import()
{
// 檢查檔案是否有上傳
$this->validate([
'file' => 'required|file|mimes:csv,xlsx,xls'
]);
if ($this->canCreate) {
$import = new ArtistDataImport();
Excel::import($import, $this->file);
$success = $import->successCount;
$fail = $import->failCount;
$this->reset('file');
$this->showModal =false;
session()->flash('message', '匯入完成:成功 $success 筆,失敗 $fail 筆。');
}
}
public function render()
{
return view('livewire.admin.artist-import-data');
}
}

View File

@ -193,7 +193,7 @@ final class ArtistTable extends PowerGridComponent
->slot(__('artists.edit'))
->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('admin.artist-form', 'openEditArtistModal', ['id' => $row->id]);
->dispatchTo('admin.artist-form', 'openModal', ['id' => $row->id]);
}
if($this->canDelect){
$actions[] =Button::add('delete')

380
config/excel.php Normal file
View File

@ -0,0 +1,380 @@
<?php
use Maatwebsite\Excel\Excel;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
return [
'exports' => [
/*
|--------------------------------------------------------------------------
| Chunk size
|--------------------------------------------------------------------------
|
| When using FromQuery, the query is automatically chunked.
| Here you can specify how big the chunk should be.
|
*/
'chunk_size' => 1000,
/*
|--------------------------------------------------------------------------
| Pre-calculate formulas during export
|--------------------------------------------------------------------------
*/
'pre_calculate_formulas' => false,
/*
|--------------------------------------------------------------------------
| Enable strict null comparison
|--------------------------------------------------------------------------
|
| When enabling strict null comparison empty cells ('') will
| be added to the sheet.
*/
'strict_null_comparison' => false,
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
*/
'csv' => [
'delimiter' => ',',
'enclosure' => '"',
'line_ending' => PHP_EOL,
'use_bom' => false,
'include_separator_line' => false,
'excel_compatibility' => false,
'output_encoding' => '',
'test_auto_detect' => true,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
],
'imports' => [
/*
|--------------------------------------------------------------------------
| Read Only
|--------------------------------------------------------------------------
|
| When dealing with imports, you might only be interested in the
| data that the sheet exists. By default we ignore all styles,
| however if you want to do some logic based on style data
| you can enable it by setting read_only to false.
|
*/
'read_only' => true,
/*
|--------------------------------------------------------------------------
| Ignore Empty
|--------------------------------------------------------------------------
|
| When dealing with imports, you might be interested in ignoring
| rows that have null values or empty strings. By default rows
| containing empty strings or empty values are not ignored but can be
| ignored by enabling the setting ignore_empty to true.
|
*/
'ignore_empty' => false,
/*
|--------------------------------------------------------------------------
| Heading Row Formatter
|--------------------------------------------------------------------------
|
| Configure the heading row formatter.
| Available options: none|slug|custom
|
*/
'heading_row' => [
'formatter' => 'slug',
],
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
*/
'csv' => [
'delimiter' => null,
'enclosure' => '"',
'escape_character' => '\\',
'contiguous' => false,
'input_encoding' => Csv::GUESS_ENCODING,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
/*
|--------------------------------------------------------------------------
| Cell Middleware
|--------------------------------------------------------------------------
|
| Configure middleware that is executed on getting a cell value
|
*/
'cells' => [
'middleware' => [
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
],
],
],
/*
|--------------------------------------------------------------------------
| Extension detector
|--------------------------------------------------------------------------
|
| Configure here which writer/reader type should be used when the package
| needs to guess the correct type based on the extension alone.
|
*/
'extension_detector' => [
'xlsx' => Excel::XLSX,
'xlsm' => Excel::XLSX,
'xltx' => Excel::XLSX,
'xltm' => Excel::XLSX,
'xls' => Excel::XLS,
'xlt' => Excel::XLS,
'ods' => Excel::ODS,
'ots' => Excel::ODS,
'slk' => Excel::SLK,
'xml' => Excel::XML,
'gnumeric' => Excel::GNUMERIC,
'htm' => Excel::HTML,
'html' => Excel::HTML,
'csv' => Excel::CSV,
'tsv' => Excel::TSV,
/*
|--------------------------------------------------------------------------
| PDF Extension
|--------------------------------------------------------------------------
|
| Configure here which Pdf driver should be used by default.
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
*/
'pdf' => Excel::DOMPDF,
],
/*
|--------------------------------------------------------------------------
| Value Binder
|--------------------------------------------------------------------------
|
| PhpSpreadsheet offers a way to hook into the process of a value being
| written to a cell. In there some assumptions are made on how the
| value should be formatted. If you want to change those defaults,
| you can implement your own default value binder.
|
| Possible value binders:
|
| [x] Maatwebsite\Excel\DefaultValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
*/
'value_binder' => [
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
],
'cache' => [
/*
|--------------------------------------------------------------------------
| Default cell caching driver
|--------------------------------------------------------------------------
|
| By default PhpSpreadsheet keeps all cell values in memory, however when
| dealing with large files, this might result into memory issues. If you
| want to mitigate that, you can configure a cell caching driver here.
| When using the illuminate driver, it will store each value in the
| cache store. This can slow down the process, because it needs to
| store each value. You can use the "batch" store if you want to
| only persist to the store when the memory limit is reached.
|
| Drivers: memory|illuminate|batch
|
*/
'driver' => 'memory',
/*
|--------------------------------------------------------------------------
| Batch memory caching
|--------------------------------------------------------------------------
|
| When dealing with the "batch" caching driver, it will only
| persist to the store when the memory limit is reached.
| Here you can tweak the memory limit to your liking.
|
*/
'batch' => [
'memory_limit' => 60000,
],
/*
|--------------------------------------------------------------------------
| Illuminate cache
|--------------------------------------------------------------------------
|
| When using the "illuminate" caching driver, it will automatically use
| your default cache store. However if you prefer to have the cell
| cache on a separate store, you can configure the store name here.
| You can use any store defined in your cache config. When leaving
| at "null" it will use the default store.
|
*/
'illuminate' => [
'store' => null,
],
/*
|--------------------------------------------------------------------------
| Cache Time-to-live (TTL)
|--------------------------------------------------------------------------
|
| The TTL of items written to cache. If you want to keep the items cached
| indefinitely, set this to null. Otherwise, set a number of seconds,
| a \DateInterval, or a callable.
|
| Allowable types: callable|\DateInterval|int|null
|
*/
'default_ttl' => 10800,
],
/*
|--------------------------------------------------------------------------
| Transaction Handler
|--------------------------------------------------------------------------
|
| By default the import is wrapped in a transaction. This is useful
| for when an import may fail and you want to retry it. With the
| transactions, the previous import gets rolled-back.
|
| You can disable the transaction handler by setting this to null.
| Or you can choose a custom made transaction handler here.
|
| Supported handlers: null|db
|
*/
'transactions' => [
'handler' => 'db',
'db' => [
'connection' => null,
],
],
'temporary_files' => [
/*
|--------------------------------------------------------------------------
| Local Temporary Path
|--------------------------------------------------------------------------
|
| When exporting and importing files, we use a temporary file, before
| storing reading or downloading. Here you can customize that path.
| permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
|
*/
'local_path' => storage_path('framework/cache/laravel-excel'),
/*
|--------------------------------------------------------------------------
| Local Temporary Path Permissions
|--------------------------------------------------------------------------
|
| Permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
| If omitted the default permissions of the filesystem will be used.
|
*/
'local_permissions' => [
// 'dir' => 0755,
// 'file' => 0644,
],
/*
|--------------------------------------------------------------------------
| Remote Temporary Disk
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup with queues in which you
| cannot rely on having a shared local temporary path, you might
| want to store the temporary file on a shared disk. During the
| queue executing, we'll retrieve the temporary file from that
| location instead. When left to null, it will always use
| the local path. This setting only has effect when using
| in conjunction with queued imports and exports.
|
*/
'remote_disk' => null,
'remote_prefix' => null,
/*
|--------------------------------------------------------------------------
| Force Resync
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup as above, it's possible
| for the clean up that occurs after entire queue has been run to only
| cleanup the server that the last AfterImportJob runs on. The rest of the server
| would still have the local temporary file stored on it. In this case your
| local storage limits can be exceeded and future imports won't be processed.
| To mitigate this you can set this config value to be true, so that after every
| queued chunk is processed the local temporary file is deleted on the server that
| processed it.
|
*/
'force_resync_remote' => null,
],
];

View File

@ -3,8 +3,9 @@
return [
'management' => '歌手管理',
'list' => '歌手列表',
'CreateNew' => '新增歌手',
'CreateNew' => '新增',
'EditArtist' => '編輯歌手',
'ImportData' => '滙入',
'create_edit' => '新增 / 編輯',
'create' => '新增',
'edit' => '編輯',

View File

@ -1,4 +1,4 @@
<x-wireui:modal-card title="{{ $artistId ? __('artists.EditArtist') : __('artists.CreateNew') }}" blur wire:model.defer="showCreateModal">
<x-wireui:modal-card title="{{ $artistId ? __('artists.EditArtist') : __('artists.CreateNew') }}" blur wire:model.defer="showModal">
<div class="space-y-4">
<x-wireui:select
label="{{__('artists.category')}}"
@ -13,7 +13,7 @@
<x-slot name="footer">
<div class="flex justify-between w-full">
<x-wireui:button flat label="{{__('artists.cancel')}}" @click="$wire.showCreateModal = false" />
<x-wireui:button flat label="{{__('artists.cancel')}}" wire:click="closeModal" />
<x-wireui:button primary label="{{__('artists.submit')}}" wire:click="save" />
</div>
</x-slot>

View File

@ -1,8 +1,15 @@
<div class="flex justify-end mb-2">
<div class="flex justify-end mb-2 mr-2 mt-2 sm:mt-0 gap-3">
<x-wireui:button
wire:click="$dispatchTo('admin.artist-form', 'openCreateArtistModal')"
wire:click="$dispatchTo('admin.artist-form', 'openModal')"
icon="plus"
label="{{ __('artists.CreateNew') }}"
class="bg-blue-600 text-white"
/>
<x-wireui:button
wire:click="$dispatchTo('admin.artist-import-data','openModal')"
icon="document-plus"
label="{{ __('artists.ImportData') }}"
class="bg-green-600 text-white"
/>
</div>

View File

@ -0,0 +1,43 @@
<x-wireui:modal-card title="{{ __('artists.ImportData') }}" blur wire:model.defer="showModal">
{{-- 說明區塊 --}}
<div class="mb-4 p-4 bg-gray-100 border border-gray-300 rounded text-sm text-gray-700">
<p class="font-semibold mb-2">匯入格式說明</p>
<p class="mb-2">請依下列表格格式準備 CSV 檔案:</p>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-left border border-collapse border-gray-300 mb-2">
<thead class="bg-gray-200">
<tr>
<th class="border border-gray-300 px-3 py-1">類別</th>
<th class="border border-gray-300 px-3 py-1">名稱</th>
</tr>
</thead>
<tbody>
<tr>
<td class="border border-gray-300 px-3 py-1"></td>
<td class="border border-gray-300 px-3 py-1">某男</td>
</tr>
<tr>
<td class="border border-gray-300 px-3 py-1"></td>
<td class="border border-gray-300 px-3 py-1">某女</td>
</tr>
</tbody>
</table>
</div>
<p class="text-gray-600 text-xs">
類別欄位僅可使用:<strong></strong><strong></strong><strong></strong><strong></strong><strong>其他</strong>
</p>
</div>
{{-- 檔案上傳 --}}
<input type="file" wire:model="file" accept=".csv, .xls, .xlsx" class="mb-4 w-full" />
<x-slot name="footer">
<div class="flex justify-between w-full">
<x-wireui:button flat label="{{ __('artists.cancel') }}" wire:click="closeModal" />
<x-wireui:button primary label="{{ __('artists.submit') }}" wire:click="import" />
</div>
</x-slot>
</x-wireui:modal-card>

View File

@ -1,6 +1,6 @@
<x-layouts.admin>
@if (session()->has('message'))
<x-wireui:notifications />
<script>
window.$wireui.notify({
@ -9,11 +9,11 @@
icon: 'success'
});
</script>
@endif
{{-- 單一 Livewire 元件,內含資料表與 Modal --}}
<livewire:admin.artist-table />
<livewire:admin.artist-form />
<livewire:admin.artist-import-data />
</x-layouts.admin>