單機版 v0.0.0 20250617
This commit is contained in:
commit
424739eea5
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[docker-compose.yml]
|
||||||
|
indent_size = 4
|
67
.env.example
Normal file
67
.env.example
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
APP_NAME=KTVSingle
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_TIMEZONE=Asia/Taipei
|
||||||
|
APP_URL=http://hzd.superstar.dnsnet.cc
|
||||||
|
L5_SWAGGER_CONST_HOST=http://hzd.superstar.dnsnet.cc/
|
||||||
|
|
||||||
|
APP_LOCALE=zh-tw
|
||||||
|
APP_FALLBACK_LOCALE=zh-tw
|
||||||
|
APP_FAKER_LOCALE=zh_TW
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=mariadb
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=KTVSingle
|
||||||
|
DB_USERNAME=KTVSingle
|
||||||
|
DB_PASSWORD=ESM7yTPMnavFmbBH
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
/.github export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
.styleci.yml export-ignore
|
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/.phpunit.cache
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/storage/api-docs/
|
||||||
|
/vendor
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/auth.json
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
.DS_Store
|
61
README.md
Normal file
61
README.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## About Laravel
|
||||||
|
|
||||||
|
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||||
|
|
||||||
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||||
|
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||||
|
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||||
|
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||||
|
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||||
|
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||||
|
|
||||||
|
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||||
|
|
||||||
|
## Learning Laravel
|
||||||
|
|
||||||
|
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||||
|
|
||||||
|
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||||
|
|
||||||
|
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||||
|
|
||||||
|
## Laravel Sponsors
|
||||||
|
|
||||||
|
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||||
|
|
||||||
|
### Premium Partners
|
||||||
|
|
||||||
|
- **[Vehikl](https://vehikl.com/)**
|
||||||
|
- **[Tighten Co.](https://tighten.co)**
|
||||||
|
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||||
|
- **[64 Robots](https://64robots.com)**
|
||||||
|
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
||||||
|
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||||
|
- **[Redberry](https://redberry.international/laravel-development/)**
|
||||||
|
- **[Active Logic](https://activelogic.com)**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||||
|
|
||||||
|
## Security Vulnerabilities
|
||||||
|
|
||||||
|
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
45
app/Console/Commands/CheckRoomOnlineStatus.php
Normal file
45
app/Console/Commands/CheckRoomOnlineStatus.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Models\MachineStatus;
|
||||||
|
use App\Enums\RoomStatus;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use App\Services\MachineStatusForwarder;
|
||||||
|
|
||||||
|
class CheckRoomOnlineStatus extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'rooms:check-online-status';
|
||||||
|
protected $description = 'Check if each room has recent MachineStatus data, mark offline if outdated';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$threshold = $now->subMinutes(10);
|
||||||
|
|
||||||
|
// 所有房間
|
||||||
|
$rooms = Room::with('branch')->where('is_online',1)->get();
|
||||||
|
|
||||||
|
foreach ($rooms as $room) {
|
||||||
|
$branch = optional($room->branch);
|
||||||
|
$latestStatus = MachineStatus::where('hostname', $room->type->value.$room->name)
|
||||||
|
->latest('created_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$latestStatus || $latestStatus->created_at < $threshold) {
|
||||||
|
$room->is_online = false;
|
||||||
|
$room->save();
|
||||||
|
$this->info("Room [{$room->name}] marked as offline (no recent MachineStatus)");
|
||||||
|
}
|
||||||
|
$response = (new MachineStatusForwarder(
|
||||||
|
$branch->external_ip ?? '',
|
||||||
|
'/api/room/receiveSwitch',
|
||||||
|
$room->toArray()
|
||||||
|
))->forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
46
app/Console/Commands/ClearMachineStatuses.php
Normal file
46
app/Console/Commands/ClearMachineStatuses.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class ClearMachineStatuses extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:clear-machine-statuses';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = '備份並清空 machine_statuses 表,每週保留一次';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$day = now()->format('l'); // e.g. "Monday"
|
||||||
|
$targetTable = "machine_statuses_" . $day;
|
||||||
|
|
||||||
|
DB::statement("CREATE TABLE IF NOT EXISTS _machine_statuses LIKE machine_statuses");
|
||||||
|
|
||||||
|
// 先刪除舊表(如存在)
|
||||||
|
DB::statement("DROP TABLE IF EXISTS {$targetTable}");
|
||||||
|
|
||||||
|
// 改名備份
|
||||||
|
DB::statement("RENAME TABLE machine_statuses TO {$targetTable}");
|
||||||
|
|
||||||
|
// 空表回命名
|
||||||
|
DB::statement("RENAME TABLE _machine_statuses TO machine_statuses");
|
||||||
|
|
||||||
|
$this->info("Machine statuses backed up to {$targetTable} and table cleared.");
|
||||||
|
}
|
||||||
|
}
|
91
app/Console/Commands/TransferSqliteToMysql.php
Normal file
91
app/Console/Commands/TransferSqliteToMysql.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
|
||||||
|
class TransferSqliteToMysql extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'transfer:sqlite {path : SQLite 相對路徑(例:sqlite/song.sqlite)} ';
|
||||||
|
|
||||||
|
protected $description = 'Transfer all data from SQLite to MySQL, optionally truncating tables first.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$path = ltrim($this->argument('path'), '/');
|
||||||
|
$this->info("[Transfer] 開始轉移 SQLite 資料:{$path}");
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
$this->error("[Transfer] 找不到 SQLite 檔案:{$path}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 動態產生唯一 connection 名稱
|
||||||
|
$connectionName = 'sqlite_' . md5($path . microtime());
|
||||||
|
|
||||||
|
config(["database.connections.{$connectionName}" => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'database' => $path,
|
||||||
|
'prefix' => '',
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$this->info("🚀 Starting transfer from SQLite to MySQL...");
|
||||||
|
|
||||||
|
// 讀取 SQLite 資料庫的所有資料表
|
||||||
|
$sqliteTables = DB::connection($connectionName)->select("
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
||||||
|
");
|
||||||
|
|
||||||
|
if (empty($sqliteTables)) {
|
||||||
|
$this->error("❌ No tables found in SQLite database.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取得 .env 中指定的 MySQL 連線名稱
|
||||||
|
$mysqlConnection = config('database.default'); // 默認會是 'mysql',如果 .env 修改會自動更新
|
||||||
|
|
||||||
|
foreach ($sqliteTables as $tableObj) {
|
||||||
|
$table = $tableObj->name;
|
||||||
|
|
||||||
|
// 忽略 Laravel 內部 migration 表
|
||||||
|
if ($table === 'migrations') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("📦 Transferring table: {$table}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 用 cursor 來避免一次性佔用過多記憶體
|
||||||
|
$rows = DB::connection($connectionName)->table($table)->cursor();
|
||||||
|
|
||||||
|
$buffer = [];
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$buffer[] = (array) $row;
|
||||||
|
$count++;
|
||||||
|
|
||||||
|
if ($count % 500 === 0) {
|
||||||
|
DB::connection($mysqlConnection)->table($table)->insert($buffer);
|
||||||
|
$buffer = []; // 清空 buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入剩下的資料
|
||||||
|
if (!empty($buffer)) {
|
||||||
|
DB::connection($mysqlConnection)->table($table)->insert($buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("✅ Done: {$table} ({$count} records)");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("❌ Failed to transfer {$table}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("🎉 Transfer complete!");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
30
app/Enums/ArtistCategory.php
Normal file
30
app/Enums/ArtistCategory.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
enum ArtistCategory: string
|
||||||
|
{
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Unset = '未定義';
|
||||||
|
case Male = '男';
|
||||||
|
case Female = '女';
|
||||||
|
case Group = '團';
|
||||||
|
case Foreign = '外';
|
||||||
|
case Other = '其他';
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Male => __('enums.Male'),
|
||||||
|
self::Female => __('enums.Female'),
|
||||||
|
self::Group => __('enums.arist.category.Group'),
|
||||||
|
self::Foreign => __('enums.arist.category.Foreign'),
|
||||||
|
self::Other => __('enums.Other'),
|
||||||
|
self::Unset => __('enums.Unset'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
33
app/Enums/RoomStatus.php
Normal file
33
app/Enums/RoomStatus.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="RoomStatus",
|
||||||
|
* type="string",
|
||||||
|
* enum={"active", "closed", "fire", "error"},
|
||||||
|
* example="error"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
enum RoomStatus: string {
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Active = 'active';
|
||||||
|
case Closed = 'closed';
|
||||||
|
case Fire ='fire';
|
||||||
|
case Error = 'error';
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Active => __('enums.room.status.Active'),
|
||||||
|
self::Closed => __('enums.room.status.Closed'),
|
||||||
|
self::Fire => __('enums.room.status.Fire'),
|
||||||
|
self::Error => __('enums.room.status.Error'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
31
app/Enums/RoomType.php
Normal file
31
app/Enums/RoomType.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="RoomType",
|
||||||
|
* type="string",
|
||||||
|
* enum={"unset", "pc", "svr"},
|
||||||
|
* example="error"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
enum RoomType: string {
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Unset = 'unset';
|
||||||
|
case PC = 'pc';
|
||||||
|
case SVR ='svr';
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Unset => __('enums.room.status.Unset'),
|
||||||
|
self::PC => "PC",
|
||||||
|
self::SVR => "SVR",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
56
app/Enums/SongLanguageType.php
Normal file
56
app/Enums/SongLanguageType.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
enum SongLanguageType: string
|
||||||
|
{
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Unset = '未定義';
|
||||||
|
case Mandarin = '國語';
|
||||||
|
case Taiwanese = '台語';
|
||||||
|
case English = '英語';
|
||||||
|
case Japanese = '日語';
|
||||||
|
case Cantonese = '粵語';
|
||||||
|
case Korean = '韓語';
|
||||||
|
case Vietnamese = '越語';
|
||||||
|
case Hakka = '客語';
|
||||||
|
case Other = '其他';
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Unset => __('enums.Unset'),
|
||||||
|
self::Mandarin => __('enums.song.LanguageType.Mandarin'),
|
||||||
|
self::Taiwanese => __('enums.song.LanguageType.Taiwanese'),
|
||||||
|
self::English => __('enums.song.LanguageType.English'),
|
||||||
|
self::Japanese => __('enums.song.LanguageType.Japanese'),
|
||||||
|
self::Cantonese => __('enums.song.LanguageType.Cantonese'),
|
||||||
|
self::Korean => __('enums.song.LanguageType.Korean'),
|
||||||
|
self::Vietnamese => __('enums.song.LanguageType.Vietnamese'),
|
||||||
|
self::Hakka => __('enums.song.LanguageType.Hakka'),
|
||||||
|
self::Other => __('enums.song.LanguageType.Other'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static function fromLabelOrName(string $input): self
|
||||||
|
{
|
||||||
|
$aliasMap = [
|
||||||
|
'英文' => '英語',
|
||||||
|
'華語' => '國語',
|
||||||
|
'普通話' => '國語',
|
||||||
|
'台灣話' => '台語',
|
||||||
|
'客家話' => '客語',
|
||||||
|
];
|
||||||
|
// 將別名轉為正式值
|
||||||
|
$input = $aliasMap[$input] ?? $input;
|
||||||
|
foreach (self::cases() as $case) {
|
||||||
|
if ($case->value === $input || $case->name === $input) {
|
||||||
|
return $case;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::Unset;
|
||||||
|
}
|
||||||
|
}
|
29
app/Enums/SongSituation.php
Normal file
29
app/Enums/SongSituation.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
enum SongSituation: string
|
||||||
|
{
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Unset = '未定義';
|
||||||
|
case Romantic = '浪漫';
|
||||||
|
case Soft = '柔和';
|
||||||
|
case Dynamic = '動感';
|
||||||
|
case Bright = '明亮';
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Unset => __('enums.Unset'),
|
||||||
|
self::Romantic => __('enums.song.situation.Romantic'),
|
||||||
|
self::Soft => __('enums.song.situation.Soft'),
|
||||||
|
self::Dynamic => __('enums.song.situation.Dynamic'),
|
||||||
|
self::Bright => __('enums.song.situation.Bright'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
app/Enums/Traits/HasLabels.php
Normal file
20
app/Enums/Traits/HasLabels.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
35
app/Enums/UserGender.php
Normal file
35
app/Enums/UserGender.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="UserGender",
|
||||||
|
* type="string",
|
||||||
|
* enum={"male", "female", "other", "unset"},
|
||||||
|
* example="male"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
enum UserGender: string
|
||||||
|
{
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Male = 'male';
|
||||||
|
case Female = 'female';
|
||||||
|
case Other = 'other';
|
||||||
|
case Unset = 'unset';
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Male => __('enums.Male'),
|
||||||
|
self::Female => __('enums.Female'),
|
||||||
|
self::Other => __('enums.Other'),
|
||||||
|
self::Unset => __('enums.Unset'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
app/Enums/UserStatus.php
Normal file
34
app/Enums/UserStatus.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Enums\Traits\HasLabels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="UserStatus",
|
||||||
|
* type="string",
|
||||||
|
* enum={"0", "1", "2"},
|
||||||
|
* description="User status: 0=Active, 1=Suspended, 2=Deleting",
|
||||||
|
* example="0"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
enum UserStatus: int
|
||||||
|
{
|
||||||
|
use HasLabels;
|
||||||
|
|
||||||
|
case Active = 0; // 正常
|
||||||
|
case Suspended = 1; // 停權
|
||||||
|
case Deleting = 2; // 刪除中
|
||||||
|
|
||||||
|
// 返回對應的顯示文字
|
||||||
|
public function labels(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::Active => __('enums.user.status.Active'),
|
||||||
|
self::Suspended => __('enums.user.status.Suspended'),
|
||||||
|
self::Deleting => __('enums.user.status.Deleting'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
192
app/Helpers/ChineseNameConverter.php
Normal file
192
app/Helpers/ChineseNameConverter.php
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use Overtrue\PHPOpenCC\OpenCC;
|
||||||
|
use Overtrue\Pinyin\Pinyin;
|
||||||
|
|
||||||
|
class ChineseNameConverter
|
||||||
|
{
|
||||||
|
public static array $pinyinToZhuyinMap = [
|
||||||
|
'a' => 'ㄚ' ,'o' => 'ㄛ' ,'e' => 'ㄜ' ,'er' => 'ㄦ','ai' => 'ㄞ' ,'ei' => 'ㄟ' ,'ao' => 'ㄠ' ,'ou' => 'ㄡ' ,'an' => 'ㄢ' ,'n' => 'ㄣ','en' => 'ㄣ'
|
||||||
|
,'ang' => 'ㄤ' ,'eng' => 'ㄥ' ,'yi' => 'ㄧ' ,'ya' => 'ㄧㄚ' ,'yo' => 'ㄧㄛ' ,'ye' => 'ㄧㄝ' ,'yai' => 'ㄧㄞ' ,'yao' => 'ㄧㄠ'
|
||||||
|
,'you' => 'ㄧㄡ' ,'yan' => 'ㄧㄢ' ,'yin' => 'ㄧㄣ' ,'yang' => 'ㄧㄤ' ,'ying' => 'ㄧㄥ' ,'wu' => 'ㄨ' ,'wa' => 'ㄨㄚ' ,'wo' => 'ㄨㄛ'
|
||||||
|
,'wai' => 'ㄨㄞ' ,'wei' => 'ㄨㄟ' ,'wan' => 'ㄨㄢ' ,'wen' => 'ㄨㄣ' ,'wang' => 'ㄨㄤ' ,'weng' => 'ㄨㄥ' ,'yu' => 'ㄩ' ,'yue' => 'ㄩㄝ'
|
||||||
|
,'yuan' => 'ㄩㄢ' ,'yun' => 'ㄩㄣ', 'yong' =>'ㄩㄥ','ba' => 'ㄅㄚ' ,'bo' => 'ㄅㄛ' ,'bai' => 'ㄅㄞ' ,'bei' => 'ㄅㄟ' ,'bao' => 'ㄅㄠ' ,'ban' => 'ㄅㄢ'
|
||||||
|
,'ben' => 'ㄅㄣ' ,'bang' => 'ㄅㄤ' ,'beng' => 'ㄅㄥ' ,'bi' => 'ㄅㄧ' ,'bie' => 'ㄅㄧㄝ' ,'biao' => 'ㄅㄧㄠ' ,'bian' => 'ㄅㄧㄢ'
|
||||||
|
,'bin' => 'ㄅㄧㄣ' ,'bing' => 'ㄅㄧㄥ' ,'bu' => 'ㄅㄨ' ,'pa' => 'ㄆㄚ' ,'po' => 'ㄆㄛ' ,'pai' => 'ㄆㄞ' ,'pei' => 'ㄆㄟ' ,'pao' => 'ㄆㄠ'
|
||||||
|
,'pou' => 'ㄆㄡ' ,'pan' => 'ㄆㄢ' ,'pen' => 'ㄆㄣ' ,'pang' => 'ㄆㄤ' ,'peng' => 'ㄆㄥ' ,'pi' => 'ㄆㄧ' ,'pie' => 'ㄆㄧㄝ' ,'piao' => 'ㄆㄧㄠ'
|
||||||
|
,'pian' => 'ㄆㄧㄢ' ,'pin' => 'ㄆㄧㄣ' ,'ping' => 'ㄆㄧㄥ' ,'pu' => 'ㄆㄨ' ,'ma' => 'ㄇㄚ' ,'mo' => 'ㄇㄛ' ,'me' => 'ㄇㄜ' ,'mai' => 'ㄇㄞ'
|
||||||
|
,'mei' => 'ㄇㄟ' ,'mao' => 'ㄇㄠ' ,'mou' => 'ㄇㄡ' ,'man' => 'ㄇㄢ' ,'men' => 'ㄇㄣ' ,'mang' => 'ㄇㄤ' ,'meng' => 'ㄇㄥ' ,'mi' => 'ㄇㄧ'
|
||||||
|
,'mie' => 'ㄇㄧㄝ' ,'miao' => 'ㄇㄧㄠ' ,'mian' => 'ㄇㄧㄢ' ,'min' => 'ㄇㄧㄣ' ,'ming' => 'ㄇㄧㄥ' ,'mu' => 'ㄇㄨ' ,'fa' => 'ㄈㄚ'
|
||||||
|
,'fo' => 'ㄈㄛ' ,'fei' => 'ㄈㄟ' ,'fou' => 'ㄈㄡ' ,'fan' => 'ㄈㄢ' ,'fen' => 'ㄈㄣ' ,'fang' => 'ㄈㄤ' ,'feng' => 'ㄈㄥ' ,'fu' => 'ㄈㄨ'
|
||||||
|
,'da' => 'ㄉㄚ' ,'de' => 'ㄉㄜ' ,'dai' => 'ㄉㄞ' ,'dei' => 'ㄉㄟ' ,'dao' => 'ㄉㄠ' ,'dou' => 'ㄉㄡ' ,'dan' => 'ㄉㄢ' ,'den' => 'ㄉㄣ'
|
||||||
|
,'dang' => 'ㄉㄤ' ,'deng' => 'ㄉㄥ' ,'di' => 'ㄉㄧ' ,'die' => 'ㄉㄧㄝ' ,'diao' => 'ㄉㄧㄠ' ,'diu' => 'ㄉㄧㄡ' ,'dian' => 'ㄉㄧㄢ'
|
||||||
|
,'ding' => 'ㄉㄧㄥ' ,'du' => 'ㄉㄨ' ,'duo' => 'ㄉㄨㄛ' ,'dui' => 'ㄉㄨㄟ' ,'duan' => 'ㄉㄨㄢ' ,'dun' => 'ㄉㄨㄣ' ,'dong' => 'ㄉㄨㄥ'
|
||||||
|
,'ta' => 'ㄊㄚ' ,'te' => 'ㄊㄜ' ,'tai' => 'ㄊㄞ' ,'tao' => 'ㄊㄠ' ,'tou' => 'ㄊㄡ' ,'tan' => 'ㄊㄢ' ,'tang' => 'ㄊㄤ' ,'teng' => 'ㄊㄥ'
|
||||||
|
,'ti' => 'ㄊㄧ' ,'tie' => 'ㄊㄧㄝ' ,'tiao' => 'ㄊㄧㄠ' ,'tian' => 'ㄊㄧㄢ' ,'ting' => 'ㄊㄧㄥ' ,'tu' => 'ㄊㄨ' ,'tuo' => 'ㄊㄨㄛ'
|
||||||
|
,'tui' => 'ㄊㄨㄟ' ,'tuan' => 'ㄊㄨㄢ' ,'tun' => 'ㄊㄨㄣ' ,'tong' => 'ㄊㄨㄥ' ,'na' => 'ㄋㄚ' ,'ne' => 'ㄋㄜ' ,'nai' => 'ㄋㄞ'
|
||||||
|
,'nei' => 'ㄋㄟ' ,'nao' => 'ㄋㄠ' ,'nou' => 'ㄋㄡ' ,'nan' => 'ㄋㄢ' ,'nen' => 'ㄋㄣ' ,'nang' => 'ㄋㄤ' ,'neng' => 'ㄋㄥ' ,'ni' => 'ㄋㄧ'
|
||||||
|
,'nie' => 'ㄋㄧㄝ' ,'niao' => 'ㄋㄧㄠ' ,'niu' => 'ㄋㄧㄡ' ,'nian' => 'ㄋㄧㄢ' ,'nin' => 'ㄋㄧㄣ' ,'niang' => 'ㄋㄧㄤ' ,'ning' => 'ㄋㄧㄥ'
|
||||||
|
,'nu' => 'ㄋㄨ' ,'nuo' => 'ㄋㄨㄛ' ,'nuan' => 'ㄋㄨㄢ' ,'nun' => 'ㄋㄨㄣ' ,'nong' => 'ㄋㄨㄥ' ,'nv' => 'ㄋㄩ' ,'nue' => 'ㄋㄩㄝ' ,'la' => 'ㄌㄚ'
|
||||||
|
,'le' => 'ㄌㄜ' ,'lai' => 'ㄌㄞ' ,'lei' => 'ㄌㄟ' ,'lao' => 'ㄌㄠ' ,'lou' => 'ㄌㄡ' ,'lan' => 'ㄌㄢ' ,'lang' => 'ㄌㄤ' ,'leng' => 'ㄌㄥ'
|
||||||
|
,'li' => 'ㄌㄧ' ,'lia' => 'ㄌㄧㄚ' ,'lie' => 'ㄌㄧㄝ' ,'liao' => 'ㄌㄧㄠ' ,'liu' => 'ㄌㄧㄡ' ,'lian' => 'ㄌㄧㄢ' ,'lin' => 'ㄌㄧㄣ'
|
||||||
|
,'liang' => 'ㄌㄧㄤ' ,'ling' => 'ㄌㄧㄥ' ,'lu' => 'ㄌㄨ' ,'luo' => 'ㄌㄨㄛ' ,'luan' => 'ㄌㄨㄢ' ,'lun' => 'ㄌㄨㄣ' ,'long' => 'ㄌㄨㄥ'
|
||||||
|
,'lv' => 'ㄌㄩ' ,'lve' => 'ㄌㄩㄝ' ,'ga' => 'ㄍㄚ' ,'ge' => 'ㄍㄜ' ,'gai' => 'ㄍㄞ' ,'gei' => 'ㄍㄟ' ,'gao' => 'ㄍㄠ' ,'gou' => 'ㄍㄡ'
|
||||||
|
,'gan' => 'ㄍㄢ' ,'gen' => 'ㄍㄣ' ,'gang' => 'ㄍㄤ' ,'geng' => 'ㄍㄥ' ,'gu' => 'ㄍㄨ' ,'gua' => 'ㄍㄨㄚ' ,'guo' => 'ㄍㄨㄛ' ,'guai' => 'ㄍㄨㄞ'
|
||||||
|
,'gui' => 'ㄍㄨㄟ' ,'guan' => 'ㄍㄨㄢ' ,'gun' => 'ㄍㄨㄣ' ,'guang' => 'ㄍㄨㄤ' ,'gong' => 'ㄍㄨㄥ' ,'ka' => 'ㄎㄚ' ,'ke' => 'ㄎㄜ'
|
||||||
|
,'kai' => 'ㄎㄞ' ,'kei' => 'ㄎㄟ' ,'kao' => 'ㄎㄠ' ,'kou' => 'ㄎㄡ' ,'kan' => 'ㄎㄢ' ,'ken' => 'ㄎㄣ' ,'kang' => 'ㄎㄤ' ,'keng' => 'ㄎㄥ'
|
||||||
|
,'ku' => 'ㄎㄨ' ,'kua' => 'ㄎㄨㄚ' ,'kuo' => 'ㄎㄨㄛ' ,'kuai' => 'ㄎㄨㄞ' ,'kui' => 'ㄎㄨㄟ' ,'kuan' => 'ㄎㄨㄢ' ,'kun' => 'ㄎㄨㄣ'
|
||||||
|
,'kuang' => 'ㄎㄨㄤ' ,'kong' => 'ㄎㄨㄥ' ,'ha' => 'ㄏㄚ' ,'he' => 'ㄏㄜ' ,'hai' => 'ㄏㄞ' ,'hei' => 'ㄏㄟ' ,'hao' => 'ㄏㄠ' ,'hou' => 'ㄏㄡ'
|
||||||
|
,'han' => 'ㄏㄢ' ,'hen' => 'ㄏㄣ' ,'hang' => 'ㄏㄤ' ,'heng' => 'ㄏㄥ' ,'hu' => 'ㄏㄨ' ,'hua' => 'ㄏㄨㄚ' ,'huo' => 'ㄏㄨㄛ' ,'huai' => 'ㄏㄨㄞ'
|
||||||
|
,'hui' => 'ㄏㄨㄟ' ,'huan' => 'ㄏㄨㄢ' ,'hun' => 'ㄏㄨㄣ' ,'huang' => 'ㄏㄨㄤ' ,'hong' => 'ㄏㄨㄥ' ,'ji' => 'ㄐ一' ,'jia' => 'ㄐㄧㄚ'
|
||||||
|
,'jie' => 'ㄐㄧㄝ' ,'jiao' => 'ㄐㄧㄠ' ,'jiu' => 'ㄐ一ㄡ' ,'jian' => 'ㄐ一ㄢ' ,'jin' => 'ㄐ一ㄣ' ,'jiang' => 'ㄐ一ㄤ' ,'jing' => 'ㄐ一ㄥ'
|
||||||
|
,'ju' => 'ㄐㄩ' ,'jue' => 'ㄐㄩㄝ' ,'juan' => 'ㄐㄩㄢ' ,'jun' => 'ㄐㄩㄣ' ,'jiong' => 'ㄐㄩㄥ' ,'qi' => 'ㄑ一' ,'qia' => 'ㄑㄧㄚ'
|
||||||
|
,'qie' => 'ㄑㄧㄝ' ,'qiao' => 'ㄑㄧㄠ' ,'qiu' => 'ㄑ一ㄡ' ,'qian' => 'ㄑ一ㄢ' ,'qin' => 'ㄑ一ㄣ' ,'qiang' => 'ㄑ一ㄤ' ,'qing' => 'ㄑ一ㄥ'
|
||||||
|
,'qu' => 'ㄑㄩ' ,'que' => 'ㄑㄩㄝ' ,'quan' => 'ㄑㄩㄢ' ,'qun' => 'ㄑㄩㄣ' ,'qiong' => 'ㄑㄩㄥ' ,'xi' => 'ㄒ一' ,'xia' => 'ㄒㄧㄚ'
|
||||||
|
,'xie' => 'ㄒㄧㄝ' ,'xiao' => 'ㄒㄧㄠ' ,'xiu' => 'ㄒ一ㄡ' ,'xian' => 'ㄒ一ㄢ' ,'xin' => 'ㄒ一ㄣ' ,'xiang' => 'ㄒ一ㄤ' ,'xing' => 'ㄒ一ㄥ'
|
||||||
|
,'xu' => 'ㄒㄩ' ,'xue' => 'ㄒㄩㄝ' ,'xuan' => 'ㄒㄩㄢ' ,'xun' => 'ㄒㄩㄣ' ,'xiong' => 'ㄒㄩㄥ' ,'zha' => 'ㄓㄚ' ,'zhe' => 'ㄓㄜ'
|
||||||
|
,'zhai' => 'ㄓㄞ' ,'zhei' => 'ㄓㄟ' ,'zhao' => 'ㄓㄠ' ,'zhou' => 'ㄓㄡ' ,'zhan' => 'ㄓㄢ' ,'zhen' => 'ㄓㄣ' ,'zhang' => 'ㄓㄤ'
|
||||||
|
,'zheng' => 'ㄓㄥ' ,'zhu' => 'ㄓㄨ' ,'zhua' => 'ㄓㄨㄚ' ,'zhuo' => 'ㄓㄨㄛ' ,'zhuai' => 'ㄓㄨㄞ' ,'zhui' => 'ㄓㄨㄟ' ,'zhuan' => 'ㄓㄨㄢ'
|
||||||
|
,'zhun' => 'ㄓㄨㄣ' ,'zhuang' => 'ㄓㄨㄤ' ,'zhong' => 'ㄓㄨㄥ' ,'cha' => 'ㄔㄚ' ,'che' => 'ㄔㄜ' ,'chai' => 'ㄔㄞ' ,'chao' => 'ㄔㄠ'
|
||||||
|
,'chou' => 'ㄔㄡ' ,'chan' => 'ㄔㄢ' ,'chen' => 'ㄔㄣ' ,'chang' => 'ㄔㄤ' ,'cheng' => 'ㄔㄥ' ,'chu' => 'ㄔㄨ' ,'chua' => 'ㄔㄨㄚ'
|
||||||
|
,'chuo' => 'ㄔㄨㄛ' ,'chuai' => 'ㄔㄨㄞ' ,'chui' => 'ㄔㄨㄟ' ,'chuan' => 'ㄔㄨㄢ' ,'chun' => 'ㄔㄨㄣ' ,'chuang' => 'ㄔㄨㄤ' ,'chong' => 'ㄔㄨㄥ'
|
||||||
|
,'sha' => 'ㄕㄚ' ,'she' => 'ㄕㄜ' ,'shai' => 'ㄕㄞ' ,'shao' => 'ㄕㄠ' ,'shou' => 'ㄕㄡ' ,'shan' => 'ㄕㄢ' ,'shen' => 'ㄕㄣ' ,'shang' => 'ㄕㄤ'
|
||||||
|
,'sheng' => 'ㄕㄥ' ,'shu' => 'ㄕㄨ' ,'shua' => 'ㄕㄨㄚ' ,'shuo' => 'ㄕㄨㄛ' ,'shuai' => 'ㄕㄨㄞ' ,'shui' => 'ㄕㄨㄟ' ,'shuan' => 'ㄕㄨㄢ'
|
||||||
|
,'shun' => 'ㄕㄨㄣ' ,'shuang' => 'ㄕㄨㄤ' ,'shong' => 'ㄕㄨㄥ' ,'re' => 'ㄖㄜ' ,'rai' => 'ㄖㄞ' ,'rao' => 'ㄖㄠ' ,'rou' => 'ㄖㄡ' ,'ran' => 'ㄖㄢ'
|
||||||
|
,'ren' => 'ㄖㄣ' ,'rang' => 'ㄖㄤ' ,'reng' => 'ㄖㄥ' ,'ru' => 'ㄖㄨ' ,'ruo' => 'ㄖㄨㄛ' ,'rui' => 'ㄖㄨㄟ' ,'ruan' => 'ㄖㄨㄢ' ,'run' => 'ㄖㄨㄣ'
|
||||||
|
,'rong' => 'ㄖㄨㄥ' ,'za' => 'ㄗㄚ' ,'ze' => 'ㄗㄜ' ,'zai' => 'ㄗㄞ' ,'zei' => 'ㄗㄟ' ,'zao' => 'ㄗㄠ' ,'zou' => 'ㄗㄡ' ,'zan' => 'ㄗㄢ'
|
||||||
|
,'zen' => 'ㄗㄣ' ,'zang' => 'ㄗㄤ' ,'zeng' => 'ㄗㄥ' ,'zu' => 'ㄗㄨ' ,'zuo' => 'ㄗㄨㄛ' ,'zuan' => 'ㄗㄨㄢ' ,'zun' => 'ㄗㄨㄣ' ,'zong' => 'ㄗㄨㄥ'
|
||||||
|
,'zui' => 'ㄗㄨㄟ' ,'ca' => 'ㄘㄚ' ,'ce' => 'ㄘㄜ' ,'cai' => 'ㄘㄞ' ,'cao' => 'ㄘㄠ' ,'cou' => 'ㄘㄡ' ,'can' => 'ㄘㄢ' ,'cen' => 'ㄘㄣ' ,'cang' => 'ㄘㄤ'
|
||||||
|
,'ceng' => 'ㄘㄥ' ,'cu' => 'ㄘㄨ' ,'cuo' => 'ㄘㄨㄛ' ,'cuan' => 'ㄘㄨㄢ' ,'cun' => 'ㄘㄨㄣ' ,'cong' => 'ㄘㄨㄥ' ,'cui' => 'ㄘㄨㄟ' ,'sa' => 'ㄙㄚ'
|
||||||
|
,'se' => 'ㄙㄜ' ,'sai' => 'ㄙㄞ' ,'sao' => 'ㄙㄠ' ,'sou' => 'ㄙㄡ' ,'san' => 'ㄙㄢ' ,'sen' => 'ㄙㄣ' ,'sang' => 'ㄙㄤ' ,'seng' => 'ㄙㄥ' ,'su' => 'ㄙㄨ'
|
||||||
|
,'suo' => 'ㄙㄨㄛ' ,'suan' => 'ㄙㄨㄢ' ,'sun' => 'ㄙㄨㄣ' ,'song' => 'ㄙㄨㄥ' ,'sui' => 'ㄙㄨㄟ' ,'zi' => 'ㄗ' ,'ci' => 'ㄘ' ,'si' => 'ㄙ'
|
||||||
|
,'zhi' => 'ㄓ' ,'chi' => 'ㄔ' ,'shi' => 'ㄕ' ,'ri' => 'ㄖ'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove tone marks from Pinyin
|
||||||
|
*
|
||||||
|
* @param string $pinyin
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function normalizePinyin(string $pinyin): string {
|
||||||
|
$map = [
|
||||||
|
'ā'=>'a', 'á'=>'a', 'ǎ'=>'a', 'à'=>'a',
|
||||||
|
'ē'=>'e', 'é'=>'e', 'ě'=>'e', 'è'=>'e',
|
||||||
|
'ī'=>'i', 'í'=>'i', 'ǐ'=>'i', 'ì'=>'i',
|
||||||
|
'ō'=>'o', 'ó'=>'o', 'ǒ'=>'o', 'ò'=>'o',
|
||||||
|
'ū'=>'u', 'ú'=>'u', 'ǔ'=>'u', 'ù'=>'u',
|
||||||
|
//'ǖ'=>'ü','ǘ'=>'ü','ǚ'=>'ü','ǜ'=>'ü',
|
||||||
|
'ǖ'=>'u','ǘ'=>'u','ǚ'=>'u','ǜ'=>'u',
|
||||||
|
'ǹ'=>'n',
|
||||||
|
|
||||||
|
];
|
||||||
|
return strtr($pinyin, $map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Traditional Chinese characters to Simplified Chinese characters
|
||||||
|
*
|
||||||
|
* @param string $traditional
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function convertToSimplified(string $traditional): string
|
||||||
|
{
|
||||||
|
return OpenCC::convert($traditional, 'TW2S');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert Chinese characters to Pinyin initials
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getPinyinAbbr(string $text): string
|
||||||
|
{
|
||||||
|
return strtoupper(Pinyin::abbr($text)->join(''));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert Chinese characters to full Pinyin
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function getPinyinSentence(string $text): string
|
||||||
|
{
|
||||||
|
return Pinyin::sentence($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getZhuyinAbbr(string $text): string
|
||||||
|
{
|
||||||
|
|
||||||
|
$segments = explode(' ', self::convertPinyinToZhuyin($text));
|
||||||
|
$zhuyinAbbr = '';
|
||||||
|
foreach ($segments as $z) {
|
||||||
|
$firstChar = mb_substr($z, 0, 1, 'UTF-8');
|
||||||
|
$zhuyinAbbr .= self::isZhuyin($firstChar) ? $firstChar : strtoupper($z);
|
||||||
|
}
|
||||||
|
return $zhuyinAbbr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Pinyin to Zhuyin
|
||||||
|
*
|
||||||
|
* @param string $pinyin
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function convertPinyinToZhuyin(string $pinyin): string
|
||||||
|
{
|
||||||
|
$zhuyin = [];
|
||||||
|
$words = explode(' ', $pinyin);
|
||||||
|
|
||||||
|
foreach ($words as $word) {
|
||||||
|
$key = self::normalizePinyin(strtolower($word));
|
||||||
|
$zhuyin[] = self::$pinyinToZhuyinMap[$key] ?? $word;
|
||||||
|
}
|
||||||
|
return implode(' ', $zhuyin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isZhuyin(string $char): bool
|
||||||
|
{
|
||||||
|
if (empty($char)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$code = mb_ord($char, 'UTF-8');
|
||||||
|
return $code >= 0x3105 && $code <= 0x3129;
|
||||||
|
}
|
||||||
|
public static function getKTVPinyinAbbr(string $text): string
|
||||||
|
{
|
||||||
|
$pinyin = '';
|
||||||
|
$chars = mb_str_split($text);
|
||||||
|
foreach ($chars as $char) {
|
||||||
|
if (preg_match("/\p{Han}/u", $char)) {
|
||||||
|
$pinyin .= self::getPinyinAbbr($char);
|
||||||
|
} else {
|
||||||
|
$pinyin .= $char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pinyin;
|
||||||
|
}
|
||||||
|
public static function getKTVZhuyinAbbr(string $text): string
|
||||||
|
{
|
||||||
|
$zhuyin = '';
|
||||||
|
$chars = mb_str_split($text);
|
||||||
|
foreach ($chars as $char) {
|
||||||
|
if (preg_match("/\p{Han}/u", $char)) {
|
||||||
|
$zhuyin .= self::getZhuyinAbbr(Pinyin::sentence($char));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $zhuyin;
|
||||||
|
}
|
||||||
|
public static function convertAll(string $text): array
|
||||||
|
{
|
||||||
|
$simplified =self::convertToSimplified($text);
|
||||||
|
$pinyin = self::getPinyinSentence($simplified);
|
||||||
|
return [
|
||||||
|
'simplified' => $simplified,
|
||||||
|
'pinyin_abbr' => self::getKTVPinyinAbbr($simplified),
|
||||||
|
'pinyin_full' => $pinyin,
|
||||||
|
'zhuyin' => self::getKTVZhuyinAbbr($simplified),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
1006
app/Helpers/ChineseStrokesConverter.php
Normal file
1006
app/Helpers/ChineseStrokesConverter.php
Normal file
File diff suppressed because it is too large
Load Diff
61
app/Http/Controllers/ArtistController.php
Normal file
61
app/Http/Controllers/ArtistController.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Artist;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use OpenApi\Annotations as OA;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(
|
||||||
|
* name="Artist",
|
||||||
|
* description="藝人資料搜尋"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ArtistController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/artists/search",
|
||||||
|
* tags={"Artist"},
|
||||||
|
* summary="搜尋藝人名稱",
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="search",
|
||||||
|
* in="query",
|
||||||
|
* description="關鍵字搜尋藝人名稱",
|
||||||
|
* required=false,
|
||||||
|
* @OA\Schema(type="string")
|
||||||
|
* ),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="selected",
|
||||||
|
* in="query",
|
||||||
|
* description="已選擇藝人 ID 陣列",
|
||||||
|
* required=false,
|
||||||
|
* @OA\Schema(type="array", @OA\Items(type="integer"))
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="搜尋結果"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Auth\Events\Verified;
|
||||||
|
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class VerifyEmailController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Mark the authenticated user's email address as verified.
|
||||||
|
*/
|
||||||
|
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->user()->hasVerifiedEmail()) {
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
123
app/Http/Controllers/AuthController.php
Normal file
123
app/Http/Controllers/AuthController.php
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ReceiveLoginRequest;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use App\Models\User;
|
||||||
|
use OpenApi\Annotations as OA;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(
|
||||||
|
* name="Auth",
|
||||||
|
* description="用戶個人資料"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/login",
|
||||||
|
* tags={"Auth"},
|
||||||
|
* summary="登入取得 Token",
|
||||||
|
* description="使用帳號密碼登入並回傳 JWT Token。",
|
||||||
|
* operationId="login",
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ReceiveLoginRequest")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功傳送指令並回傳 TCP 回應",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="data", type="object",
|
||||||
|
* @OA\Property(property="token", type="string", example="eyJhbGciOiJIUz...")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||||
|
* @OA\Property(property="data", type="null")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function login(ReceiveLoginRequest $request)
|
||||||
|
{
|
||||||
|
if (!Auth::attempt($request->only('email', 'password'))) {
|
||||||
|
return ApiResponse::unauthorized();
|
||||||
|
}
|
||||||
|
$user = Auth::user();
|
||||||
|
// 3. 產生或取得 Token
|
||||||
|
if (empty($user->api_plain_token)) {
|
||||||
|
$token = $user->createToken('*')->plainTextToken;
|
||||||
|
$user->api_plain_token = $token;
|
||||||
|
$user->save();
|
||||||
|
} else {
|
||||||
|
$token = $user->api_plain_token;
|
||||||
|
}
|
||||||
|
return ApiResponse::success(['token' => $token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/profile",
|
||||||
|
* summary="Get current user profile",
|
||||||
|
* tags={"Auth"},
|
||||||
|
* security={{"Authorization":{}}},
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="User profile",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/User")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||||
|
* @OA\Property(property="data", type="null")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="Accept",
|
||||||
|
* in="header",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(type="string", default="application/json")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function profile(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::success($request->user());
|
||||||
|
}
|
||||||
|
}
|
92
app/Http/Controllers/BranchControlController.php
Normal file
92
app/Http/Controllers/BranchControlController.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Enums\RoomType;
|
||||||
|
use App\Enums\RoomStatus;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
/**
|
||||||
|
* @OA\Tag(
|
||||||
|
* name="Branch",
|
||||||
|
* description="分店包廂"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class BranchControlController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/branches",
|
||||||
|
* tags={"Branch"},
|
||||||
|
* summary="取得分店資訊",
|
||||||
|
* description="需帶入 Bearer Token 取得當前用戶可存取的分店資訊",
|
||||||
|
* operationId="Branches",
|
||||||
|
* security={{"Authorization":{}}},
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功回傳分店列表",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* @OA\Property(property="branches", type="array", @OA\Items(
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||||
|
* @OA\Property(property="enable", type="boolean", example=true),
|
||||||
|
* @OA\Property(
|
||||||
|
* property="rooms",
|
||||||
|
* type="object",
|
||||||
|
* additionalProperties={"$ref": "#/components/schemas/RoomInfo"}
|
||||||
|
* )
|
||||||
|
* ))
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||||
|
* @OA\Property(property="data", type="null")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="RoomInfo",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="is_online", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="status", ref="#/components/schemas/RoomStatus"),
|
||||||
|
* @OA\Property(property="started_at", type="string", format="date-time", example="2025-06-04T10:00:00Z"),
|
||||||
|
* @OA\Property(property="ended_at", type="string", format="date-time", example="2025-06-04T12:00:00Z")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function Branches(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$branches = Branch::with('rooms')->get()->map(function ($branch) {
|
||||||
|
return [
|
||||||
|
'id' => $branch->id,
|
||||||
|
'branch_name' => $branch->name,
|
||||||
|
'enable' => $branch->enable,
|
||||||
|
'rooms' => $branch->rooms
|
||||||
|
->filter(fn($room) => $room->type === RoomType::PC)
|
||||||
|
->mapWithKeys(function ($room) {
|
||||||
|
$roomName = ($room->type?->value ?? '') . $room->name;
|
||||||
|
return [
|
||||||
|
$roomName => [
|
||||||
|
'is_online' => $room->is_online,
|
||||||
|
'status' => $room->status?->value ?? null,
|
||||||
|
'started_at' => $room->started_at,
|
||||||
|
'ended_at' => $room->ended_at,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
return ApiResponse::success(['branches' => $branches]);
|
||||||
|
}
|
||||||
|
}
|
32
app/Http/Controllers/Controller.php
Normal file
32
app/Http/Controllers/Controller.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Info(
|
||||||
|
* title="My API",
|
||||||
|
* version="1.0.0",
|
||||||
|
* description="API documentation using L5 Swagger"
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Server(
|
||||||
|
* url=L5_SWAGGER_CONST_HOST,
|
||||||
|
* description="API Server"
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\SecurityScheme(
|
||||||
|
* securityScheme="Authorization",
|
||||||
|
* type="http",
|
||||||
|
* scheme="bearer",
|
||||||
|
* bearerFormat="JWT"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class Controller extends BaseController
|
||||||
|
{
|
||||||
|
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
||||||
|
}
|
316
app/Http/Controllers/RoomControlController.php
Normal file
316
app/Http/Controllers/RoomControlController.php
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\SendRoomSwitchCommandRequest;
|
||||||
|
use App\Http\Requests\ReceiveSwitchRequest;
|
||||||
|
use App\Services\TcpSocketClient;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Enums\RoomStatus;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
/**
|
||||||
|
* @OA\Tag(
|
||||||
|
* name="Auth",
|
||||||
|
* description="包廂控制"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class RoomControlController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/room/receiveRegister",
|
||||||
|
* summary="包廂註冊控制指令",
|
||||||
|
* description="依據傳入的 branch_id 與 room_name,知道過來的設備來之於那個IP設備。",
|
||||||
|
* operationId="registerRoomCommand",
|
||||||
|
* tags={"Room Control"},
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ReceiveRoomRegisterRequest")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功傳送指令並回傳 TCP 回應",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/Room")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||||
|
* @OA\Property(property="data", type="null")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="Accept",
|
||||||
|
* in="header",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(type="string", default="application/json")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function receiveRegister(ReceiveRoomRegisterRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
// 1. 驗證帳密(登入用)
|
||||||
|
$credentials = $request->only('email', 'password');
|
||||||
|
|
||||||
|
if (!Auth::attempt($credentials)) {
|
||||||
|
return ApiResponse::unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 取得登入使用者
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// 3. 產生或取得 Token
|
||||||
|
if (empty($user->api_plain_token)) {
|
||||||
|
$token = $user->createToken('pc-heartbeat')->plainTextToken;
|
||||||
|
$user->api_plain_token = $token;
|
||||||
|
$user->save();
|
||||||
|
} else {
|
||||||
|
$token = $user->api_plain_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 驗證其他註冊欄位
|
||||||
|
$validated = $request->validated(); // branch_id, room_name, room_ip
|
||||||
|
|
||||||
|
// 5. 找出對應包廂
|
||||||
|
$roomType = null;
|
||||||
|
$roomName = null;
|
||||||
|
// 從 room_name(例如 PC101, SVR01)中擷取 type 與 name
|
||||||
|
if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['room_name'], $matches)) {
|
||||||
|
$roomType = strtolower($matches[1]); // 'PC' → 'pc'
|
||||||
|
$roomName = $matches[2]; // '101'
|
||||||
|
}
|
||||||
|
$branch=Branch::where('name',$validated['branch_name'])->first();
|
||||||
|
$room = Room::where('branch_id', $branch->id)
|
||||||
|
->where('name', $roomName)
|
||||||
|
->where('type', $roomType)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$room) {
|
||||||
|
return ApiResponse::error('找不到對應包廂');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新包廂資訊
|
||||||
|
$room->internal_ip = $validated['room_ip'];
|
||||||
|
$room->port = 1000; // 預設值
|
||||||
|
$room->is_online =1;
|
||||||
|
$room->touch(); // 更新 updated_at
|
||||||
|
$room->save();
|
||||||
|
|
||||||
|
// 7. 回傳 token 與包廂資料
|
||||||
|
return ApiResponse::success([
|
||||||
|
'token' => $token,
|
||||||
|
'room' => $room,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/room/heartbeat",
|
||||||
|
* summary="包廂心跳封包指令",
|
||||||
|
* description="記錄設備連線狀況",
|
||||||
|
* operationId="heartbeatRoomCommand",
|
||||||
|
* tags={"Room Control"},
|
||||||
|
* security={{"Authorization":{}}},
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ReceiveRoomStatusDataRequest")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功傳送指令並回傳 TCP 回應",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/MachineStatus")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||||
|
* @OA\Property(property="data", type="null")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="Accept",
|
||||||
|
* in="header",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(type="string", default="application/json")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function StatusReport(ReceiveRoomStatusDataRequest $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
$roomType = null;
|
||||||
|
$roomName = null;
|
||||||
|
// 從 room_name(例如 PC101, SVR01)中擷取 type 與 name
|
||||||
|
if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['hostname'], $matches)) {
|
||||||
|
$roomType = strtolower($matches[1]); // 'PC' → 'pc'
|
||||||
|
$roomName = $matches[2]; // '101'
|
||||||
|
}
|
||||||
|
$branch=Branch::where('name',$validated['branch_name'])->first();
|
||||||
|
$room = Room::where('branch_id', $branch->id)
|
||||||
|
->where('name', $roomName)
|
||||||
|
->where('type', $roomType)
|
||||||
|
->first();
|
||||||
|
// 決定 status 欄位值
|
||||||
|
$validated['status']= 'error';
|
||||||
|
if($room){
|
||||||
|
$validated['status']= 'online';
|
||||||
|
if($room->internal_ip != $validated['ip']){
|
||||||
|
$room->internal_ip = $validated['ip'];
|
||||||
|
$validated['status']='error';
|
||||||
|
}
|
||||||
|
$room->is_online=1;
|
||||||
|
$room->touch(); // 更新 updated_at
|
||||||
|
$room->save();
|
||||||
|
$response = (
|
||||||
|
new MachineStatusForwarder(
|
||||||
|
$branch->external_ip ?? '',
|
||||||
|
'/api/room/receiveSwitch',
|
||||||
|
$room->toArray()
|
||||||
|
)
|
||||||
|
)->forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'data' => MachineStatus::create($validated),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/room/sendSwitch",
|
||||||
|
* summary="送出包廂控制指令",
|
||||||
|
* description="依據傳入的 room_id 與 command,透過 TCP 傳送對應指令給包廂電腦。",
|
||||||
|
* operationId="sendRoomSwitchCommand",
|
||||||
|
* tags={"Room Control"},
|
||||||
|
* security={{"Authorization":{}}},
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/SendRoomSwitchCommandRequest")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="成功傳送指令並回傳 TCP 回應",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/Room")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||||
|
* @OA\Property(property="data", type="null")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="Accept",
|
||||||
|
* in="header",
|
||||||
|
* required=true,
|
||||||
|
* @OA\Schema(type="string", default="application/json")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function sendSwitch(SendRoomSwitchCommandRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
$branch = Branch::where('name',$validated['branch_name'])->first();
|
||||||
|
$roomType = null;
|
||||||
|
$roomName = null;
|
||||||
|
// 從 room_name(例如 PC101, SVR01)中擷取 type 與 name
|
||||||
|
if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['room_name'], $matches)) {
|
||||||
|
$roomType = strtolower($matches[1]); // 'PC' → 'pc'
|
||||||
|
$roomName = $matches[2]; // '101'
|
||||||
|
}
|
||||||
|
$room = Room::where('branch_id', $branch->id)
|
||||||
|
->where('name', $roomName)
|
||||||
|
->where('type', $roomType)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$room) {
|
||||||
|
return ApiResponse::error('房間不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查必要欄位是否缺失或狀態為錯誤
|
||||||
|
if (empty($room->internal_ip) || empty($room->port)) {
|
||||||
|
return ApiResponse::error('房間未設定 IP 或 Port');
|
||||||
|
}
|
||||||
|
|
||||||
|
$suffix = substr($room->name, -3) ?: $room->name;
|
||||||
|
$signal = match ($validated['command']) {
|
||||||
|
'active' => 'O',
|
||||||
|
'closed' => 'X',
|
||||||
|
'fire' => 'F',
|
||||||
|
default => 'X', // fallback 保險起見
|
||||||
|
};
|
||||||
|
$data = $suffix . "," . $signal;
|
||||||
|
|
||||||
|
//dd($data);
|
||||||
|
$client = new TcpSocketClient($room->internal_ip, $room->port);
|
||||||
|
try {
|
||||||
|
$response = $client->send($data);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
logger()->error('❌ TCP 傳送失敗: ' . $e->getMessage(), [
|
||||||
|
'room_id' => $room->id,
|
||||||
|
'ip' => $room->internal_ip,
|
||||||
|
'port' => $room->port,
|
||||||
|
]);
|
||||||
|
$validated['command']="error";
|
||||||
|
$validated['started_at']=null;
|
||||||
|
$validated['ended_at']=null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room->status=$validated['command'];
|
||||||
|
$room->started_at=$validated['started_at'];
|
||||||
|
$room->ended_at=$validated['ended_at'];
|
||||||
|
$room->save();
|
||||||
|
$response = (new MachineStatusForwarder(
|
||||||
|
$branch->external_ip,
|
||||||
|
"/api/room/receiveSwitch",
|
||||||
|
$room->toArray()
|
||||||
|
))->forward();
|
||||||
|
return $validated['command']==='error' ? ApiResponse::error('機房控制失敗') : ApiResponse::success($room);
|
||||||
|
}
|
||||||
|
}
|
31
app/Http/Requests/ApiRequest.php
Normal file
31
app/Http/Requests/ApiRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use App\Http\Requests\Traits\FailedValidationJsonResponse;
|
||||||
|
|
||||||
|
class ApiRequest extends FormRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
use FailedValidationJsonResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize(): bool{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules(): array{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
app/Http/Requests/ReceiveLoginRequest.php
Normal file
29
app/Http/Requests/ReceiveLoginRequest.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="ReceiveLoginRequest",
|
||||||
|
* required={"email" ,"password"},
|
||||||
|
* @OA\Property(property="email", type="string", example="XX@gmail.com"),
|
||||||
|
* @OA\Property(property="password", type="string", example="XXX"),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ReceiveLoginRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => 'required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
35
app/Http/Requests/ReceiveRoomRegisterRequest.php
Normal file
35
app/Http/Requests/ReceiveRoomRegisterRequest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="ReceiveRoomRegisterRequest",
|
||||||
|
* required={"branch_name", "room_name", "email" ,"password"},
|
||||||
|
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||||
|
* @OA\Property(property="room_name", type="string", example="PC101"),
|
||||||
|
* @OA\Property(property="room_ip", type="string", example="192.168.1.1"),
|
||||||
|
* @OA\Property(property="email", type="string", example="XX@gmail.com"),
|
||||||
|
* @OA\Property(property="password", type="string", example="XXX"),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ReceiveRoomRegisterRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'branch_name' =>'required|string|exists:branches,name',
|
||||||
|
'room_name' => 'required|string',
|
||||||
|
'room_ip' => 'required|string',
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => 'required',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
37
app/Http/Requests/ReceiveRoomStatusDataRequest.php
Normal file
37
app/Http/Requests/ReceiveRoomStatusDataRequest.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="ReceiveRoomStatusDataRequest",
|
||||||
|
* required={"branch_name","hostname", "ip", "status"},
|
||||||
|
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||||
|
* @OA\Property(property="hostname", type="string", example="PC101"),
|
||||||
|
* @OA\Property(property="ip", type="string", example="192.168.XX.XX"),
|
||||||
|
* @OA\Property(property="cpu", type="numeric", example="0.00"),
|
||||||
|
* @OA\Property(property="memory", type="numeric", example="25603"),
|
||||||
|
* @OA\Property(property="disk", type="numeric", example="158266.49"),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ReceiveRoomStatusDataRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'branch_name' =>'required|string|exists:branches,name',
|
||||||
|
'hostname' => 'required|string',
|
||||||
|
'ip' => 'required|string',
|
||||||
|
'cpu' => 'nullable|numeric',
|
||||||
|
'memory' => 'nullable|numeric',
|
||||||
|
'disk' => 'nullable|numeric',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
47
app/Http/Requests/ReceiveSwitchRequest.php
Normal file
47
app/Http/Requests/ReceiveSwitchRequest.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="ReceiveSwitchRequest",
|
||||||
|
* required={"id","branch_id", "floor", "type", "name", "is_online", "status"},
|
||||||
|
* @OA\Property(property="id", type="integer", example="1"),
|
||||||
|
* @OA\Property(property="branch_id", type="integer", example="1"),
|
||||||
|
* @OA\Property(property="floor", type="integer", example=2),
|
||||||
|
* @OA\Property(property="type", type="string", enum={"unset", "pc", "svr"}, example="pc"),
|
||||||
|
* @OA\Property(property="name", type="string", example="102"),
|
||||||
|
* @OA\Property(property="internal_ip", type="string", nullable=true, example="192.168.1.100"),
|
||||||
|
* @OA\Property(property="port", type="integer", nullable=true, example=3389),
|
||||||
|
* @OA\Property(property="is_online", type="integer", enum={0, 1}, example=1),
|
||||||
|
* @OA\Property(property="status", type="string", enum={"active", "closed", "fire", "error"}, example="active"),
|
||||||
|
* @OA\Property(property="started_at", type="string", format="date-time", nullable=true, example="2025-05-19 09:31:00"),
|
||||||
|
* @OA\Property(property="ended_at", type="string", format="date-time", nullable=true, example="2025-05-19 12:00:00")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ReceiveSwitchRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => ['required','integer','exists:rooms,id'],
|
||||||
|
'branch_id' => ['required','integer','exists:branches,id'],
|
||||||
|
'floor' => ['required','integer'],
|
||||||
|
'type' => ['required','in:unset,pc,svr'],
|
||||||
|
'name' => ['required','string'],
|
||||||
|
'internal_ip' => ['nullable','string'],
|
||||||
|
'port' => ['nullable','integer'],
|
||||||
|
'is_online' => ['required','integer'],
|
||||||
|
'status' => ['required','in:active,closed,fire,error'],
|
||||||
|
'started_at' => ['nullable','date'],
|
||||||
|
'ended_at' => ['nullable','date'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
35
app/Http/Requests/SendRoomSwitchCommandRequest.php
Normal file
35
app/Http/Requests/SendRoomSwitchCommandRequest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="SendRoomSwitchCommandRequest",
|
||||||
|
* required={"branch_name", "room_name", "command"},
|
||||||
|
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||||
|
* @OA\Property(property="room_name", type="string", example="pc102"),
|
||||||
|
* @OA\Property(property="command", type="string", enum={"active", "closed", "fire", "error"}, example="active"),
|
||||||
|
* @OA\Property(property="started_at", type="string", nullable=true, example="2025-05-19 09:31:00"),
|
||||||
|
* @OA\Property(property="ended_at", type="string", nullable=true, example="2025-05-19 09:31:00")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class SendRoomSwitchCommandRequest extends ApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'branch_name' => ['required','string','exists:branches,name'],
|
||||||
|
'room_name' => ['required','string'],
|
||||||
|
'command' => ['required','in:active,closed,fire,error'],
|
||||||
|
'started_at'=> ['nullable','date_format:Y-m-d H:i:s'],
|
||||||
|
'ended_at' => ['nullable','date_format:Y-m-d H:i:s'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
20
app/Http/Requests/Traits/FailedValidationJsonResponse.php
Normal file
20
app/Http/Requests/Traits/FailedValidationJsonResponse.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\Validator;
|
||||||
|
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
trait FailedValidationJsonResponse
|
||||||
|
{
|
||||||
|
protected function failedValidation(Validator $validator)
|
||||||
|
{
|
||||||
|
throw new HttpResponseException(response()->json([
|
||||||
|
'message' => 'Validation failed.',
|
||||||
|
'errors' => $validator->errors(),
|
||||||
|
'code' => 'ERROR',
|
||||||
|
'token' => ''
|
||||||
|
], Response::HTTP_UNPROCESSABLE_ENTITY));
|
||||||
|
}
|
||||||
|
}
|
39
app/Http/Responses/ApiResponse.php
Normal file
39
app/Http/Responses/ApiResponse.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Responses;
|
||||||
|
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="ApiResponse",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="code", type="string", example="OK"),
|
||||||
|
* @OA\Property(property="message", type="string", example="Success"),
|
||||||
|
* @OA\Property(property="data", type="object", nullable=true)
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ApiResponse
|
||||||
|
{
|
||||||
|
public static function success($data = null, string $message = 'Success', string $code = 'OK', int $status = Response::HTTP_OK): JsonResponse
|
||||||
|
{
|
||||||
|
return self::respond($code, $message, $data, $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function error(string $message = 'Error', string $code = 'ERROR', int $status = Response::HTTP_BAD_REQUEST): JsonResponse
|
||||||
|
{
|
||||||
|
return self::respond($code, $message, null, $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function unauthorized(string $message = 'Unauthorized'): JsonResponse
|
||||||
|
{
|
||||||
|
return self::error($message, 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function respond(string $code, string $message, $data, int $status): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json(['code' => $code,'message' => $message,'data' => $data,], $status);
|
||||||
|
}
|
||||||
|
}
|
48
app/Imports/DataImport.php
Normal file
48
app/Imports/DataImport.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Imports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Maatwebsite\Excel\Imports\HeadingRowFormatter;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use App\Jobs\ImportArtistChunkJob;
|
||||||
|
use App\Jobs\ImportSongChunkJob;
|
||||||
|
|
||||||
|
class DataImport implements ToCollection, WithHeadingRow, WithChunkReading
|
||||||
|
{
|
||||||
|
protected int $con=0;
|
||||||
|
protected string $modelName;
|
||||||
|
public function __construct(string $modelName)
|
||||||
|
{
|
||||||
|
HeadingRowFormatter::default('none');
|
||||||
|
$this->modelName= $modelName;
|
||||||
|
}
|
||||||
|
public function collection(Collection $rows)
|
||||||
|
{
|
||||||
|
|
||||||
|
Log::warning('匯入啟動', [
|
||||||
|
'model' => $this->modelName,
|
||||||
|
'rows_id' =>++$this->con,
|
||||||
|
'rows_count' => $rows->count()
|
||||||
|
]);
|
||||||
|
if($this->modelName=='Song'){
|
||||||
|
ImportSongChunkJob::dispatch($rows,$this->con);
|
||||||
|
}else if($this->modelName=='Artist'){
|
||||||
|
ImportArtistChunkJob::dispatch($rows,$this->con);
|
||||||
|
}else{
|
||||||
|
Log::warning('未知的 modelName', ['model' => $this->modelName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function chunkSize(): int
|
||||||
|
{
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headingRow(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
80
app/Jobs/ExportSqliteSongLibraryCacheJob.php
Normal file
80
app/Jobs/ExportSqliteSongLibraryCacheJob.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Song;
|
||||||
|
use App\Models\SongLibraryCache;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ExportSqliteSongLibraryCacheJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// 安全清空資料(兼容 SQLite 和其他資料庫)
|
||||||
|
if (Schema::hasTable('song_library_cache')) {
|
||||||
|
if (DB::getDriverName() === 'sqlite') {
|
||||||
|
SongLibraryCache::query()->delete();
|
||||||
|
} else {
|
||||||
|
DB::table('song_library_cache')->truncate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalInserted = 0;
|
||||||
|
|
||||||
|
Song::with(['artists', 'categories'])->chunk(500, function ($songs) use (&$totalInserted) {
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($songs as $song) {
|
||||||
|
$sortedArtists = $song->artists->sortBy('id')->values();
|
||||||
|
$artistA = $sortedArtists->get(0);
|
||||||
|
$artistB = $sortedArtists->get(1);
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'song_id' => $song->id,
|
||||||
|
'song_name' => $song->name,
|
||||||
|
'song_simplified' => $song->simplified,
|
||||||
|
'phonetic_abbr' => $song->phonetic_abbr ?? '',
|
||||||
|
'pinyin_abbr' => $song->pinyin_abbr ?? '',
|
||||||
|
'strokes_abbr' => $song->strokes_abbr ?? 0,
|
||||||
|
'song_number' => $song->song_number ?? 0,
|
||||||
|
'artistA' => $artistA?->name,
|
||||||
|
'artistB' => $artistB?->name,
|
||||||
|
'artistA_simplified' => $artistA?->simplified,
|
||||||
|
'artistB_simplified' => $artistB?->simplified,
|
||||||
|
'artistA_category' => $artistA?->category?->value ?? '未定義',
|
||||||
|
'artistB_category' => $artistB?->category?->value ?? '未定義',
|
||||||
|
'artist_category' => in_array(\App\Enums\ArtistCategory::Group->value, [
|
||||||
|
$artistA?->category?->value,
|
||||||
|
$artistB?->category?->value,
|
||||||
|
]) ? '團' : '未定義',
|
||||||
|
'song_filename' => $song->filename,
|
||||||
|
'song_category' => $song->categories->pluck('code')->unique()->sort()->implode(', '),
|
||||||
|
'language_name' => $song->language_type ?? '未定義',
|
||||||
|
'add_date' => $song->adddate,
|
||||||
|
'situation' => $song->situation?->value ?? '未定義',
|
||||||
|
'vocal' => $song->vocal,
|
||||||
|
'db_change' => $song->db_change,
|
||||||
|
'song_counts' => $song->song_counts ?? 0,
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
collect($rows)->chunk(1000)->each(function ($chunk) use (&$totalInserted) {
|
||||||
|
SongLibraryCache::insert($chunk->toArray());
|
||||||
|
$totalInserted += $chunk->count();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 你也可以 log 或通知插入結果
|
||||||
|
// logger("Exported {$totalInserted} songs to cache.");
|
||||||
|
}
|
||||||
|
}
|
87
app/Jobs/ImportArtistChunkJob.php
Normal file
87
app/Jobs/ImportArtistChunkJob.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Artist;
|
||||||
|
use App\Enums\ArtistCategory;
|
||||||
|
use App\Helpers\ChineseNameConverter;
|
||||||
|
use App\Helpers\ChineseStrokesConverter;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ImportArtistChunkJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected Collection $rows;
|
||||||
|
protected String $id;
|
||||||
|
|
||||||
|
public function __construct(Collection $rows,String $id)
|
||||||
|
{
|
||||||
|
$this->rows = $rows;
|
||||||
|
$this->id = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
Log::warning('匯入啟動', [
|
||||||
|
'model' => "ImportArtistChunkJob",
|
||||||
|
'rows_id' =>$this->id,
|
||||||
|
]);
|
||||||
|
$now = now();
|
||||||
|
foreach ($this->rows as $index => $row) {
|
||||||
|
try {
|
||||||
|
$name = $this->normalizeName($row['歌手姓名'] ?? '');
|
||||||
|
|
||||||
|
if (empty($name) || Artist::where('name', $name)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字元處理
|
||||||
|
$simplified = ChineseNameConverter::convertToSimplified($name);
|
||||||
|
if (!$row->has('歌手注音')) {
|
||||||
|
$phoneticAbbr = ChineseNameConverter::getKTVZhuyinAbbr($simplified);
|
||||||
|
} else {
|
||||||
|
$phoneticAbbr = trim($row['歌手注音'] ?? '');
|
||||||
|
}
|
||||||
|
$pinyinAbbr = ChineseNameConverter::getKTVPinyinAbbr($simplified);
|
||||||
|
if (!$row->has('歌手筆畫')) {
|
||||||
|
$chars = preg_split('//u', $name, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$firstChar = $chars[0] ?? null;
|
||||||
|
$strokesAbbr = ( $firstChar && preg_match('/\p{Han}/u', $firstChar) ) ? ChineseStrokesConverter::getStrokes($firstChar) : 0;
|
||||||
|
} else {
|
||||||
|
$strokesAbbr = trim($row['歌手筆畫'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 準備 song 資料
|
||||||
|
$toInsert[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'category' => ArtistCategory::tryFrom(trim($row['歌手分類'] ?? '未定義')) ?? ArtistCategory::Unset,
|
||||||
|
'simplified' => $simplified,
|
||||||
|
'phonetic_abbr' => $phoneticAbbr,
|
||||||
|
'pinyin_abbr' => $pinyinAbbr,
|
||||||
|
'strokes_abbr' => $strokesAbbr,
|
||||||
|
'enable' =>trim($row['狀態'] ?? 1),
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Log::error("Row {$index} failed: {$e->getMessage()}", [
|
||||||
|
'row' => $row,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Artist::insert($toInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeName(?string $str): string
|
||||||
|
{
|
||||||
|
return strtoupper(mb_convert_kana(trim($str ?? ''), 'as'));
|
||||||
|
}
|
||||||
|
}
|
59
app/Jobs/ImportJob.php
Normal file
59
app/Jobs/ImportJob.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use App\Imports\DataImport;
|
||||||
|
|
||||||
|
class ImportJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected string $modelName;
|
||||||
|
protected string $filePath;
|
||||||
|
public $timeout = 3600;
|
||||||
|
public $tries = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filePath,string $modelName)
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
$this->modelName= $modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
ini_set('memory_limit', '512M'); // ✅ 增加記憶體限制
|
||||||
|
Log::info('[ImportJob] 開始處理檔案:' . $this->filePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!Storage::exists($this->filePath)) {
|
||||||
|
Log::warning('[ImportJob] 檔案不存在:' . $this->filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Excel::import(new DataImport($this->modelName), $this->filePath);
|
||||||
|
Log::info('[ImportJob] 已提交所有 chunk 匯入任務。');
|
||||||
|
|
||||||
|
Storage::delete($this->filePath);
|
||||||
|
Log::info('[ImportJob] 已刪除檔案:' . $this->filePath);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error("[ImportJob] 匯入失敗:{$e->getMessage()}", [
|
||||||
|
'file' => $this->filePath,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
208
app/Jobs/ImportSongChunkJob.php
Normal file
208
app/Jobs/ImportSongChunkJob.php
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Song;
|
||||||
|
use App\Models\Artist;
|
||||||
|
use App\Models\SongCategory;
|
||||||
|
use App\Enums\ArtistCategory;
|
||||||
|
use App\Enums\SongLanguageType;
|
||||||
|
use App\Enums\SongSituation;
|
||||||
|
use App\Helpers\ChineseNameConverter;
|
||||||
|
use App\Helpers\ChineseStrokesConverter;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ImportSongChunkJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected Collection $rows;
|
||||||
|
protected String $id;
|
||||||
|
protected array $categoryMap = [];
|
||||||
|
protected array $artistCache =[];
|
||||||
|
|
||||||
|
public function __construct(Collection $rows,String $id)
|
||||||
|
{
|
||||||
|
$this->rows = $rows;
|
||||||
|
$this->id = $id;
|
||||||
|
$this->categoryMap = SongCategory::pluck('id', 'code')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
Log::warning('匯入啟動', [
|
||||||
|
'model' => "ImportSongChunkJob",
|
||||||
|
'rows_id' =>$this->id,
|
||||||
|
]);
|
||||||
|
$ToInsert = [];
|
||||||
|
$artistMap = [];
|
||||||
|
$categoryMap = [];
|
||||||
|
$pMap = [
|
||||||
|
'\\\\SVR01\\DISK01\\' => 'DISK01\\',
|
||||||
|
'\\\\SVR01\\DISK02\\' => 'DISK02\\',
|
||||||
|
'\\\\SVR01\\DISK03\\' => 'DISK03\\',
|
||||||
|
'\\\\SVR01\\DISK04\\' => 'DISK04\\',
|
||||||
|
'\\\\SVR01\\DISK05\\' => 'DISK05\\',
|
||||||
|
'\\\\SVR01\\DISK06\\' => 'DISK06\\',
|
||||||
|
'\\\\SVR01\\DISK07\\' => 'DISK07\\',
|
||||||
|
'\\\\SVR01\\DISK08\\' => 'DISK08\\',
|
||||||
|
'\\\\SVR01\\DISK09\\' => 'DISK09\\',
|
||||||
|
];
|
||||||
|
foreach ($this->rows as $index => $row) {
|
||||||
|
$songId = trim($row['編號'] ?? '');
|
||||||
|
|
||||||
|
if (!$songId || Song::where('id', $songId)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 字元處理
|
||||||
|
$songName=$this->normalizeName($row['歌名'] ?? '');
|
||||||
|
$simplified=ChineseNameConverter::convertToSimplified($songName);// 繁體轉簡體
|
||||||
|
if (!$row->has('注音')) {
|
||||||
|
$phoneticAbbr = ChineseNameConverter::getKTVZhuyinAbbr($simplified);// 注音符號
|
||||||
|
} else {
|
||||||
|
$phoneticAbbr = trim($row['注音'] ?? '');
|
||||||
|
}
|
||||||
|
if (!$row->has('拼音')) {
|
||||||
|
$pinyinAbbr = ChineseNameConverter::getKTVPinyinAbbr($simplified);// 拼音首字母
|
||||||
|
} else {
|
||||||
|
$pinyinAbbr = trim($row['拼音'] ?? '');
|
||||||
|
}
|
||||||
|
if (!$row->has('kk3')) {//歌名第一個字筆畫
|
||||||
|
$chars = preg_split('//u', $songName, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$firstChar = $chars[0] ?? null;
|
||||||
|
$strokesAbbr=( $firstChar && preg_match('/\p{Han}/u', $firstChar) )? ChineseStrokesConverter::getStrokes($firstChar) : 0;
|
||||||
|
} else {
|
||||||
|
$strokesAbbr=trim($row['kk3'] ?? 0);
|
||||||
|
}
|
||||||
|
if (!$row->has('kk4')) {//歌名字數
|
||||||
|
$songNumber = mb_strlen($songName, 'UTF-8');
|
||||||
|
} else {
|
||||||
|
$songNumber=trim($row['kk4'] ?? 0);
|
||||||
|
}
|
||||||
|
$pathKey = trim($row['路徑01'] ?? '');
|
||||||
|
$disk = $pMap[$pathKey] ?? '';
|
||||||
|
$filename = trim($row['檔名'] ?? '');
|
||||||
|
// 準備 song 資料
|
||||||
|
$ToInsert[] = [
|
||||||
|
'id' => $songId,
|
||||||
|
'name' => $this->formatText($row['歌名']),
|
||||||
|
'adddate' => $this->parseExcelDate($row['日期'] ?? null),
|
||||||
|
'filename' => $disk . $filename,
|
||||||
|
//'language_type' => SongLanguageType::tryFrom(trim($row['語別'] ?? '')) ?? SongLanguageType::Unset,
|
||||||
|
'language_type' => SongLanguageType::fromLabelOrName($row['語別'] ?? ''),
|
||||||
|
'db_change' => trim($row['kk2'] ?? 0),//分貝增減
|
||||||
|
'vocal' => trim($row['kk6'] ?? 0),//人聲
|
||||||
|
'situation' => SongSituation::tryFrom(trim($row['kk7'] ?? '')) ?? SongSituation::Unset,//情境
|
||||||
|
'copyright01' => trim($row['版權01'] ?? ''),
|
||||||
|
'copyright02' => trim($row['版權02'] ?? ''),
|
||||||
|
'note01' => trim($row['版權03'] ?? ''),
|
||||||
|
'note02' => trim($row['版權04'] ?? ''),
|
||||||
|
'note03' => trim($row['版權05'] ?? ''),
|
||||||
|
'note04' => trim($row['版權06'] ?? ''),
|
||||||
|
'enable' => trim($row['狀態'] ?? 1),
|
||||||
|
'simplified' => $simplified,
|
||||||
|
'phonetic_abbr' => $phoneticAbbr,
|
||||||
|
'pinyin_abbr' => $pinyinAbbr,
|
||||||
|
'strokes_abbr' => $strokesAbbr,
|
||||||
|
'song_number' => $songNumber,
|
||||||
|
'song_counts' => trim($row['點播次數'] ?? 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 處理關聯 - 歌手
|
||||||
|
$artistIds = [];
|
||||||
|
foreach (['歌星A', '歌星B'] as $key) {
|
||||||
|
$artistName = $this->normalizeName($row[$key] ?? '');
|
||||||
|
if ($artistName === '') continue;
|
||||||
|
|
||||||
|
// 若是歌星B,且與歌星A相同,則跳過
|
||||||
|
if ($key === '歌星B' && $artistName === $this->normalizeName($row['歌星A'] ?? '')) continue;
|
||||||
|
|
||||||
|
$artistMap[$songId][] = $this->getOrCreateArtistId($artistName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分類處理(多個用 , 分隔)
|
||||||
|
if (!empty($row['分類'])) {
|
||||||
|
$categoryIds = [];
|
||||||
|
$codes = explode(',', $row['分類']);
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$code = trim($code);
|
||||||
|
if (isset($this->categoryMap[$code])) {
|
||||||
|
$categoryMap[$songId][] = $this->categoryMap[$code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Log::error("Row {$index} failed: {$e->getMessage()}", [
|
||||||
|
'row' => $row,
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 寫入資料庫
|
||||||
|
Song::insert($ToInsert);
|
||||||
|
// 同步關聯(建議可用事件或批次處理)
|
||||||
|
foreach ($artistMap as $songId => $artistIds) {
|
||||||
|
$song = Song::find($songId);
|
||||||
|
if ($song) {
|
||||||
|
$song->artists()->sync($artistIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($categoryMap as $songId => $categoryIds) {
|
||||||
|
$song = Song::find($songId);
|
||||||
|
if ($song) {
|
||||||
|
$song->categories()->sync($categoryIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private function getOrCreateArtistId(string $name): int
|
||||||
|
{
|
||||||
|
if (isset($this->artistCache[$name])) {
|
||||||
|
return $this->artistCache[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$artist = Artist::firstOrCreate(
|
||||||
|
['name' => $name],
|
||||||
|
['category' => ArtistCategory::Unset]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->artistCache[$name] = $artist->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeName(?string $str): string
|
||||||
|
{
|
||||||
|
return strtoupper(mb_convert_kana(trim($str ?? ''), 'as'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formatText($value)
|
||||||
|
{
|
||||||
|
if (is_numeric($value) && $value < 1 && $value > 0) {
|
||||||
|
// 嘗試判斷為時間類型的小數,轉為時間字串
|
||||||
|
$time = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value);
|
||||||
|
return $time->format('H:i');
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) $value);
|
||||||
|
}
|
||||||
|
private function parseExcelDate($value): ?string
|
||||||
|
{
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return \Carbon\Carbon::createFromFormat('Y-m-d', '1900-01-01')
|
||||||
|
->addDays((int)$value - 2)
|
||||||
|
->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return \Carbon\Carbon::parse($value)->format('Y-m-d');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
app/Livewire/Actions/Logout.php
Normal file
20
app/Livewire/Actions/Logout.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Actions;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
|
class Logout
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Log the current user out of the application.
|
||||||
|
*/
|
||||||
|
public function __invoke(): void
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
Session::invalidate();
|
||||||
|
Session::regenerateToken();
|
||||||
|
}
|
||||||
|
}
|
16
app/Livewire/Admin/Dashboard.php
Normal file
16
app/Livewire/Admin/Dashboard.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Admin;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class Dashboard extends Component
|
||||||
|
{
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
return view('livewire.admin.dashboard', compact('user'));
|
||||||
|
}
|
||||||
|
}
|
119
app/Livewire/Forms/ActivityLogTable.php
Normal file
119
app/Livewire/Forms/ActivityLogTable.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
|
|
||||||
|
final class ActivityLogTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WithExport;
|
||||||
|
public string $tableName = 'activity-log-table';
|
||||||
|
public bool $canDownload;
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
config(['livewire-powergrid.filter' => 'outside']);
|
||||||
|
//權限設定
|
||||||
|
$this->canDownload=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): array
|
||||||
|
{
|
||||||
|
if($this->canDownload ){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
if($this->canDownload){
|
||||||
|
$actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
}
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->withoutLoading()
|
||||||
|
->showToggleColumns();
|
||||||
|
//->showSoftDeletes()
|
||||||
|
//->showSearchInput()
|
||||||
|
$header->includeViewOnTop('livewire.header.activity-log-header');
|
||||||
|
$actions[]=$header;
|
||||||
|
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('changes_str', 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[] = "<strong>{$key}</strong>: {$oldValue} → {$newValue}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//dd(implode('<br>', $changes));
|
||||||
|
return implode('<br>', $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_str')->sortable(false)->searchable(false)->bodyAttribute('whitespace-normal text-sm text-gray-700');
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
Filter::inputText('causer_name')->placeholder('操作者'),
|
||||||
|
Filter::inputText('subject_type_label')->placeholder('模型'),
|
||||||
|
Filter::number('subject_id'),
|
||||||
|
Filter::inputText('description')->placeholder('動作'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
124
app/Livewire/Forms/ArtistForm.php
Normal file
124
app/Livewire/Forms/ArtistForm.php
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
use App\Models\Artist;
|
||||||
|
use App\Enums\ArtistCategory;
|
||||||
|
|
||||||
|
class ArtistForm extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal', 'deleteArtist'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
public ?int $artistId = null;
|
||||||
|
|
||||||
|
public array $categoryOptions =[];
|
||||||
|
public array $fields = [
|
||||||
|
'category' =>'unset',
|
||||||
|
'name' =>'',
|
||||||
|
'enable' => true,
|
||||||
|
];
|
||||||
|
public $selectedCategory = []; // 表單中選到的權限
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->categoryOptions = collect(ArtistCategory::cases())->map(fn ($category) => [
|
||||||
|
'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 openModal($id = null)
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$artist = Artist::findOrFail($id);
|
||||||
|
$this->artistId = $artist->id;
|
||||||
|
$this->fields = $artist->only(array_keys($this->fields));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
if ($this->artistId) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$artist = Artist::findOrFail($this->artistId);
|
||||||
|
$artist->update($this->fields);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '歌手已更新',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($this->canCreate) {
|
||||||
|
$artist = Artist::create($this->fields);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '歌手已新增',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
$this->dispatch('pg:eventRefresh-artist-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteArtist($id)
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
Artist::findOrFail($id)->delete();
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '歌手已刪除',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch('pg:eventRefresh-artist-table');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFields()
|
||||||
|
{
|
||||||
|
foreach ($this->fields as $key => $value) {
|
||||||
|
if ($key == 'category') {
|
||||||
|
$this->fields[$key] = 'unset';
|
||||||
|
} else {
|
||||||
|
$this->fields[$key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->artistId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.artist-form');
|
||||||
|
}
|
||||||
|
}
|
114
app/Livewire/Forms/ArtistImportData.php
Normal file
114
app/Livewire/Forms/ArtistImportData.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
use App\Jobs\ImportJob;
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistImportData extends Component
|
||||||
|
{
|
||||||
|
use WithFileUploads, WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
|
||||||
|
public $file;
|
||||||
|
public string $maxUploadSize;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->canCreate = Auth::user()?->can('song-edit') ?? false;
|
||||||
|
$this->maxUploadSize = $this->getMaxUploadSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal()
|
||||||
|
{
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->deleteTmpFile(); // 關閉 modal 時刪除暫存檔案
|
||||||
|
$this->reset(['file']);
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import()
|
||||||
|
{
|
||||||
|
// 檢查檔案是否有上傳
|
||||||
|
$this->validate([
|
||||||
|
'file' => 'required|file|mimes:csv,xlsx,xls'
|
||||||
|
]);
|
||||||
|
if ($this->canCreate) {
|
||||||
|
// 儲存檔案至 storage
|
||||||
|
$path = $this->file->storeAs('imports', uniqid() . '_' . $this->file->getClientOriginalName());
|
||||||
|
|
||||||
|
// 丟到 queue 執行
|
||||||
|
ImportJob::dispatch($path,'Artist');
|
||||||
|
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'info',
|
||||||
|
'title' => $this->file->getClientOriginalName(),
|
||||||
|
'description' => '已排入背景匯入作業,請稍候查看結果',
|
||||||
|
]);
|
||||||
|
$this->deleteTmpFile(); // 匯入後也順便刪除 tmp 檔
|
||||||
|
$this->reset(['file']);
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected function deleteTmpFile()
|
||||||
|
{
|
||||||
|
if($this->file!=null){
|
||||||
|
$Path = $this->file->getRealPath();
|
||||||
|
if ($Path && File::exists($Path)) {
|
||||||
|
File::delete($Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMaxUploadSize(): string
|
||||||
|
{
|
||||||
|
$uploadMax = $this->convertPHPSizeToBytes(ini_get('upload_max_filesize'));
|
||||||
|
$postMax = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
|
||||||
|
$max = min($uploadMax, $postMax);
|
||||||
|
return $this->humanFileSize($max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertPHPSizeToBytes(string $s): int
|
||||||
|
{
|
||||||
|
$s = trim($s);
|
||||||
|
$unit = strtolower($s[strlen($s) - 1]);
|
||||||
|
$bytes = (int) $s;
|
||||||
|
switch ($unit) {
|
||||||
|
case 'g':
|
||||||
|
$bytes *= 1024;
|
||||||
|
case 'm':
|
||||||
|
$bytes *= 1024;
|
||||||
|
case 'k':
|
||||||
|
$bytes *= 1024;
|
||||||
|
}
|
||||||
|
return $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function humanFileSize(int $bytes, int $decimals = 2): string
|
||||||
|
{
|
||||||
|
$sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$factor = floor((strlen((string) $bytes) - 1) / 3);
|
||||||
|
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sizes[$factor];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.artist-import-data');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
238
app/Livewire/Forms/ArtistTable.php
Normal file
238
app/Livewire/Forms/ArtistTable.php
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\Artist;
|
||||||
|
use App\Enums\ArtistCategory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Rule;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
final class ArtistTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WithExport, WireUiActions;
|
||||||
|
|
||||||
|
public string $tableName = 'artist-table';
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDownload;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
//public bool $deferLoading = true;
|
||||||
|
//public string $loadingComponent = 'components.power-grid-loading';
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if($this->canDownload || $this->canDelect){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions = [];
|
||||||
|
if($this->canDownload){
|
||||||
|
$actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
}
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->withoutLoading()
|
||||||
|
->showToggleColumns();
|
||||||
|
//->showSoftDeletes()
|
||||||
|
//->showSearchInput()
|
||||||
|
if($this->canCreate){
|
||||||
|
$header->includeViewOnTop('livewire.header.artist') ;
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return Artist::query();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('category_str', function (Artist $model) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
return Blade::render(
|
||||||
|
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
|
||||||
|
[
|
||||||
|
'options' => ArtistCategory::options(),
|
||||||
|
'modelId' => intval($model->id),
|
||||||
|
'fieldName'=>'category',
|
||||||
|
'selected' => $model->category->value
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 沒有權限就顯示對應的文字
|
||||||
|
|
||||||
|
return $model->category->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
|
||||||
|
} )
|
||||||
|
->add('name')
|
||||||
|
->add('simplified')
|
||||||
|
->add('phonetic_abbr')
|
||||||
|
->add('pinyin_abbr')
|
||||||
|
->add('strokes_abbr')
|
||||||
|
->add('enable')
|
||||||
|
->add('created_at_formatted', fn (Artist $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make(__('artists.no'), 'id');
|
||||||
|
$column[]=Column::make(__('artists.category'),'category_str', 'artists.category')->searchable();
|
||||||
|
$column[]=Column::make(__('artists.name'), 'name')->sortable()->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'name',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
);
|
||||||
|
$column[]=Column::make(__('artists.name.simplified'), 'simplified')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('artists.name.phinetic'), 'phonetic_abbr')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('artists.name.pinyin'), 'pinyin_abbr')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('artists.name.strokes'), 'strokes_abbr')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('artists.enable'), 'enable')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no');
|
||||||
|
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
|
||||||
|
$column[]=Column::action(__('artists.actions'));
|
||||||
|
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
#[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 ($fieldName == 'category' && $this->canEdit) {
|
||||||
|
$this->noUpdated($modelId,$fieldName,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[On('onUpdatedEditable')]
|
||||||
|
public function onUpdatedEditable($id, $field, $value): void
|
||||||
|
{
|
||||||
|
if ($field === 'name' && $this->canEdit) {
|
||||||
|
$this->noUpdated($id,$field,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[On('onUpdatedToggleable')]
|
||||||
|
public function onUpdatedToggleable($id, $field, $value): void
|
||||||
|
{
|
||||||
|
if (in_array($field,['enable']) && $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
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => $id.'.'.__('artists.'.$field).':'.$value,
|
||||||
|
'description' => '已經寫入',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::enumSelect('category_str','artists.category')
|
||||||
|
->datasource(ArtistCategory::cases())
|
||||||
|
->optionLabel('artists.category'),
|
||||||
|
Filter::inputText('name')->placeholder(__('artists.name')),
|
||||||
|
Filter::inputText('phonetic_abbr')->placeholder(__('artists.name.phinetic')),
|
||||||
|
Filter::inputText('pinyin_abbr')->placeholder(__('artists.name.pinyin')),
|
||||||
|
Filter::number('strokes_abbr')->thousands('.')->decimal(','),
|
||||||
|
Filter::boolean('enable')->label('✅', '❌'),
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function actions(Artist $row): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$actions[] =Button::add('edit')
|
||||||
|
->slot(__('artists.edit'))
|
||||||
|
->icon('solid-pencil-square')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.artist-form', 'openModal', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if($this->canDelect){
|
||||||
|
$actions[] =Button::add('delete')
|
||||||
|
->slot(__('artists.delete'))
|
||||||
|
->icon('solid-trash')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.artist-form', 'deleteArtist', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
public function actionRules($row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Hide button edit for ID 1
|
||||||
|
Rule::button('edit')
|
||||||
|
->when(fn($row) => $row->id === 1)
|
||||||
|
->hide(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
150
app/Livewire/Forms/BranchForm.php
Normal file
150
app/Livewire/Forms/BranchForm.php
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Enums\RoomType;
|
||||||
|
|
||||||
|
class BranchForm extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal', 'deleteBranch'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
public ?int $branchId = null;
|
||||||
|
|
||||||
|
public array $fields = [
|
||||||
|
'name' =>'',
|
||||||
|
'external_ip' =>'',
|
||||||
|
'enable' => true,
|
||||||
|
'roomNotes' =>''
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canEdit = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canDelect = Auth::user()?->can('room-delete') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal($id = null)
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$branch = Branch::findOrFail($id);
|
||||||
|
$this->branchId = $branch->id;
|
||||||
|
$this->fields = $branch->only(array_keys($this->fields));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
if ($this->branchId) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$branch = Branch::findOrFail($this->branchId);
|
||||||
|
$branch->update($this->fields);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '分店已更新',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($this->canCreate) {
|
||||||
|
$branch = Branch::create([
|
||||||
|
'name' => $this->fields['name'],
|
||||||
|
'external_ip' => $this->fields['external_ip'],
|
||||||
|
'enable' => $this->fields['enable'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 解析 roomNotes
|
||||||
|
$roomLines = explode("\n", trim($this->fields['roomNotes']));
|
||||||
|
$rooms = [];
|
||||||
|
|
||||||
|
foreach ($roomLines as $line) {
|
||||||
|
[$floor, $roomList] = explode(';', $line);
|
||||||
|
$floor = (int) filter_var($floor, FILTER_SANITIZE_NUMBER_INT); // 抽出 1F 的數字
|
||||||
|
|
||||||
|
$roomNames = array_map('trim', explode(',', $roomList));
|
||||||
|
|
||||||
|
foreach ($roomNames as $roomName) {
|
||||||
|
$type = match (true) {
|
||||||
|
str_starts_with($roomName, 'svr') => RoomType::SVR,
|
||||||
|
str_starts_with($roomName, 'pc') => RoomType::PC,
|
||||||
|
default => RoomType::Unset,
|
||||||
|
};
|
||||||
|
$rooms[] = new Room([
|
||||||
|
'floor' => $floor,
|
||||||
|
'type' => $type->value,
|
||||||
|
'name' => preg_replace('/^(pc|svr)/', '', $roomName),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 儲存所有 Room
|
||||||
|
$branch->rooms()->saveMany($rooms);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '分店已新增',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
$this->dispatch('pg:eventRefresh-branch-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteBranch($id)
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
Branch::findOrFail($id)->delete();
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '分店已刪除',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch('pg:eventRefresh-branch-table');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFields()
|
||||||
|
{
|
||||||
|
foreach ($this->fields as $key => $value) {
|
||||||
|
if ($key == 'enable') {
|
||||||
|
$this->fields[$key] = true;
|
||||||
|
} else {
|
||||||
|
$this->fields[$key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->branchId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.branch-form');
|
||||||
|
}
|
||||||
|
}
|
114
app/Livewire/Forms/BranchImportData.php
Normal file
114
app/Livewire/Forms/BranchImportData.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
use App\Jobs\ImportJob;
|
||||||
|
|
||||||
|
|
||||||
|
class BranchImportData extends Component
|
||||||
|
{
|
||||||
|
use WithFileUploads, WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
|
||||||
|
public $file;
|
||||||
|
public string $maxUploadSize;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->maxUploadSize = $this->getMaxUploadSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal()
|
||||||
|
{
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->deleteTmpFile(); // 關閉 modal 時刪除暫存檔案
|
||||||
|
$this->reset(['file']);
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import()
|
||||||
|
{
|
||||||
|
// 檢查檔案是否有上傳
|
||||||
|
$this->validate([
|
||||||
|
'file' => 'required|file|mimes:csv,xlsx,xls'
|
||||||
|
]);
|
||||||
|
if ($this->canCreate) {
|
||||||
|
// 儲存檔案至 storage
|
||||||
|
$path = $this->file->storeAs('imports', uniqid() . '_' . $this->file->getClientOriginalName());
|
||||||
|
|
||||||
|
// 丟到 queue 執行
|
||||||
|
ImportJob::dispatch($path,'Branch');
|
||||||
|
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'info',
|
||||||
|
'title' => $this->file->getClientOriginalName(),
|
||||||
|
'description' => '已排入背景匯入作業,請稍候查看結果',
|
||||||
|
]);
|
||||||
|
$this->deleteTmpFile(); // 匯入後也順便刪除 tmp 檔
|
||||||
|
$this->reset(['file']);
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected function deleteTmpFile()
|
||||||
|
{
|
||||||
|
if($this->file!=null){
|
||||||
|
$Path = $this->file->getRealPath();
|
||||||
|
if ($Path && File::exists($Path)) {
|
||||||
|
File::delete($Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMaxUploadSize(): string
|
||||||
|
{
|
||||||
|
$uploadMax = $this->convertPHPSizeToBytes(ini_get('upload_max_filesize'));
|
||||||
|
$postMax = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
|
||||||
|
$max = min($uploadMax, $postMax);
|
||||||
|
return $this->humanFileSize($max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertPHPSizeToBytes(string $s): int
|
||||||
|
{
|
||||||
|
$s = trim($s);
|
||||||
|
$unit = strtolower($s[strlen($s) - 1]);
|
||||||
|
$bytes = (int) $s;
|
||||||
|
switch ($unit) {
|
||||||
|
case 'g':
|
||||||
|
$bytes *= 1024;
|
||||||
|
case 'm':
|
||||||
|
$bytes *= 1024;
|
||||||
|
case 'k':
|
||||||
|
$bytes *= 1024;
|
||||||
|
}
|
||||||
|
return $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function humanFileSize(int $bytes, int $decimals = 2): string
|
||||||
|
{
|
||||||
|
$sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$factor = floor((strlen((string) $bytes) - 1) / 3);
|
||||||
|
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sizes[$factor];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.branch-import-data');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
219
app/Livewire/Forms/BranchTable.php
Normal file
219
app/Livewire/Forms/BranchTable.php
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\Branch;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Rule;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
final class BranchTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WithExport, WireUiActions;
|
||||||
|
public string $tableName = 'branch-table';
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDownload;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
config(['livewire-powergrid.filter' => 'outside']);
|
||||||
|
//權限設定
|
||||||
|
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canEdit = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canDownload=Auth::user()?->can('room-delete') ?? false;
|
||||||
|
$this->canDelect = Auth::user()?->can('room-delete') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): array
|
||||||
|
{
|
||||||
|
if($this->canDownload || $this->canDelect){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
if($this->canDownload){
|
||||||
|
$actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
}
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->withoutLoading()
|
||||||
|
->showToggleColumns();
|
||||||
|
//->showSoftDeletes()
|
||||||
|
//->showSearchInput()
|
||||||
|
if($this->canCreate){
|
||||||
|
$header->includeViewOnTop('livewire.header.branch') ;
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return Branch::query();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('name')
|
||||||
|
->add('external_ip')
|
||||||
|
->add('enable')
|
||||||
|
->add('created_at_formatted', fn (Branch $model) => Carbon::parse($model->created_at)->format('d/m/Y H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make(__('branches.no'), 'id');
|
||||||
|
$column[]=Column::make(__('branches.name'), 'name')->sortable()->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'name',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
);
|
||||||
|
$column[]=Column::make(__('branches.external_ip'), 'external_ip')->sortable()->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'external_ip',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
);
|
||||||
|
$column[]=Column::make(__('branches.enable'), 'enable')
|
||||||
|
->toggleable(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
trueLabel: 'yes',
|
||||||
|
falseLabel: 'no'
|
||||||
|
);
|
||||||
|
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
|
||||||
|
$column[]=Column::action(__('branches.actions'));
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
#[On('bulkDelete.{tableName}')]
|
||||||
|
public function bulkDelete(): void
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
|
||||||
|
if($this->checkboxValues){
|
||||||
|
Branch::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 ($fieldName == 'category' && $this->canEdit) {
|
||||||
|
$this->noUpdated($modelId,$fieldName,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[On('onUpdatedEditable')]
|
||||||
|
public function onUpdatedEditable($id, $field, $value): void
|
||||||
|
{
|
||||||
|
if ($field === 'name' && $this->canEdit) {
|
||||||
|
$this->noUpdated($id,$field,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[On('onUpdatedToggleable')]
|
||||||
|
public function onUpdatedToggleable($id, $field, $value): void
|
||||||
|
{
|
||||||
|
if (in_array($field,['enable']) && $this->canEdit) {
|
||||||
|
$this->noUpdated($id,$field,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private function noUpdated($id,$field,$value){
|
||||||
|
$branch = Branch::find($id);
|
||||||
|
if ($branch) {
|
||||||
|
$branch->{$field} = $value;
|
||||||
|
$branch->save(); // 明確觸發 saving
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => $id.'.'.__('branches.'.$field).':'.$value,
|
||||||
|
'description' => '已經寫入',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::inputText('name')->placeholder(__('branches.name')),
|
||||||
|
Filter::inputText('external_ip')->placeholder(__('branches.external_ip')),
|
||||||
|
Filter::boolean('enable')->label('✅', '❌'),
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actions(Branch $row): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
$actions[] = Button::add('room-settings')
|
||||||
|
->slot('包廂設定')
|
||||||
|
->icon('solid-cog')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded bg-amber-200 text-black')
|
||||||
|
->dispatchTo('forms.room-grid', 'openModal', ['branch_id' => $row->id]);
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$actions[] =Button::add('edit')
|
||||||
|
->slot(__('branches.edit'))
|
||||||
|
->icon('solid-pencil-square')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.branch-form', 'openModal', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if($this->canDelect){
|
||||||
|
$actions[] =Button::add('delete')
|
||||||
|
->slot(__('branches.delete'))
|
||||||
|
->icon('solid-trash')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.branch-form', 'deleteBranch', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public function actionRules($row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Hide button edit for ID 1
|
||||||
|
Rule::button('edit')
|
||||||
|
->when(fn($row) => $row->id === 1)
|
||||||
|
->hide(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
72
app/Livewire/Forms/LoginForm.php
Normal file
72
app/Livewire/Forms/LoginForm.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
use Livewire\Form;
|
||||||
|
|
||||||
|
class LoginForm extends Form
|
||||||
|
{
|
||||||
|
#[Validate('required|string|email')]
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
#[Validate('required|string')]
|
||||||
|
public string $password = '';
|
||||||
|
|
||||||
|
#[Validate('boolean')]
|
||||||
|
public bool $remember = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'form.email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the authentication request is not rate limited.
|
||||||
|
*/
|
||||||
|
protected function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout(request()));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'form.email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the authentication rate limiting throttle key.
|
||||||
|
*/
|
||||||
|
protected function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||||
|
}
|
||||||
|
}
|
96
app/Livewire/Forms/MachineStatusTable.php
Normal file
96
app/Livewire/Forms/MachineStatusTable.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\MachineStatus;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
|
||||||
|
final class MachineStatusTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
public string $tableName = 'machine-status-table';
|
||||||
|
public bool $canDownload;
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
config(['livewire-powergrid.filter' => 'outside']);
|
||||||
|
//權限設定
|
||||||
|
$this->canDownload=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): array
|
||||||
|
{
|
||||||
|
if($this->canDownload ){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
if($this->canDownload){
|
||||||
|
$actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
}
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->withoutLoading()
|
||||||
|
->showToggleColumns();
|
||||||
|
|
||||||
|
$header->includeViewOnTop('livewire.header.admin.machine-status');
|
||||||
|
|
||||||
|
$actions[]=$header;
|
||||||
|
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function datasource(): Builder
|
||||||
|
{
|
||||||
|
return MachineStatus::query()->latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('hostname')
|
||||||
|
->add('ip')
|
||||||
|
->add('cpu')
|
||||||
|
->add('memory')
|
||||||
|
->add('disk')
|
||||||
|
->add('created_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make('Id', 'id');
|
||||||
|
$column[]=Column::make(__('machine-status.hostname'), 'hostname')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('machine-status.Ip'), 'ip')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('machine-status.Cpu'), 'cpu')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('machine-status.Memory'), 'memory')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('machine-status.Disk'), 'disk')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('machine-status.Created at'), 'created_at')->sortable()->searchable();
|
||||||
|
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
Filter::inputText('hostname')->placeholder(__('machine-status.hostname')),
|
||||||
|
Filter::inputText('ip')->placeholder(__('machine-status.Ip')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
115
app/Livewire/Forms/Modals/RoomDetailModal.php
Normal file
115
app/Livewire/Forms/Modals/RoomDetailModal.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Admin;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Models\Branch;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use App\Services\ApiClient;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
class RoomDetailModal extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = [
|
||||||
|
'openModal', 'closeModal',
|
||||||
|
'startNotify', 'stopNotify', 'fireNotify',
|
||||||
|
'openAccountNotify','closeAccountNotify'
|
||||||
|
];
|
||||||
|
|
||||||
|
public $room_name;
|
||||||
|
public $branch;
|
||||||
|
public bool $showModal = false;
|
||||||
|
|
||||||
|
public function openModal($roomId)
|
||||||
|
{
|
||||||
|
$room = Room::find($roomId);
|
||||||
|
$this->room_name=$room->type->value . $room->name;
|
||||||
|
$this->branch = Branch::find($room->branch_id);
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
public function startNotify()
|
||||||
|
{
|
||||||
|
$data = $this->buildNotifyData('active', now(), null);
|
||||||
|
$this->send($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stopNotify()
|
||||||
|
{
|
||||||
|
$data = $this->buildNotifyData('closed', null, null);
|
||||||
|
$this->send($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fireNotify()
|
||||||
|
{
|
||||||
|
$data = $this->buildNotifyData('fire', null, null);
|
||||||
|
$this->send($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openAccountNotify()
|
||||||
|
{
|
||||||
|
$data = $this->buildNotifyData('active', now(), null);
|
||||||
|
$this->send($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeAccountNotify()
|
||||||
|
{
|
||||||
|
$data = $this->buildNotifyData('closed', now(), null);
|
||||||
|
$this->send($data);
|
||||||
|
}
|
||||||
|
protected function buildNotifyData(string $command, $startedAt = null, $endedAt = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'branch_name' => $this->branch->name ?? '',
|
||||||
|
'room_name' => $this->room_name ?? '',
|
||||||
|
'command' => $command,
|
||||||
|
'started_at' => $startedAt ? $startedAt->toDateTimeString() : null,
|
||||||
|
'ended_at' => $endedAt ? $endedAt->toDateTimeString() : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(array $data){
|
||||||
|
$user = Auth::user();
|
||||||
|
$token = $user->api_plain_token ?? null;
|
||||||
|
|
||||||
|
if (!$token) {
|
||||||
|
$this->sendErrorNotification('api', 'API token is missing.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = new ApiClient(config('app.url') , $token );
|
||||||
|
$response = $api->post('/api/room/sendSwitch', $data);
|
||||||
|
if ($response->failed()) {
|
||||||
|
$this->sendErrorNotification('api', 'API request failed: ' . $response->body());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ✅ 成功提示
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '命令已成功發送:' . $data['command'],
|
||||||
|
]);
|
||||||
|
// ✅ 關閉 modal
|
||||||
|
$this->showModal = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public function sendErrorNotification(string $title = '錯誤', string $description = '發生未知錯誤')
|
||||||
|
{
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'error',
|
||||||
|
'title' => $title,
|
||||||
|
'description' =>$description,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.admin.room-detail-modal');
|
||||||
|
}
|
||||||
|
}
|
114
app/Livewire/Forms/RoleForm.php
Normal file
114
app/Livewire/Forms/RoleForm.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
class RoleForm extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openCreateRoleModal','openEditRoleModal', 'deleteRole'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public $showCreateModal=false;
|
||||||
|
public ?int $roleId = null;
|
||||||
|
public $name = '';
|
||||||
|
public $permissions = []; // 所有權限清單
|
||||||
|
public $selectedPermissions = []; // 表單中選到的權限
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->permissions = Permission::all();
|
||||||
|
$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 openCreateRoleModal()
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showCreateModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openEditRoleModal($id)
|
||||||
|
{
|
||||||
|
$role = Role::findOrFail($id);
|
||||||
|
$this->roleId = $role->id;
|
||||||
|
$this->name = $role->name;
|
||||||
|
$this->selectedPermissions = $role->permissions()->pluck('id')->toArray();
|
||||||
|
$this->showCreateModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'selectedPermissions' => 'array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->roleId) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$role = Role::findOrFail($this->roleId);
|
||||||
|
$role->update(['name' => $this->name]);
|
||||||
|
$role->syncPermissions($this->selectedPermissions);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '角色已更新',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($this->canCreate) {
|
||||||
|
$role = Role::create(['name' => $this->name]);
|
||||||
|
$role->syncPermissions($this->selectedPermissions);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '角色已新增',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showCreateModal = false;
|
||||||
|
$this->dispatch('pg:eventRefresh-role-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteRole($id)
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
Role::findOrFail($id)->delete();
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '角色已刪除',
|
||||||
|
]);
|
||||||
|
$this->dispatch('pg:eventRefresh-role-table');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFields()
|
||||||
|
{
|
||||||
|
$this->name = '';
|
||||||
|
$this->selectedPermissions = [];
|
||||||
|
$this->roleId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.role-form');
|
||||||
|
}
|
||||||
|
}
|
181
app/Livewire/Forms/RoleTable.php
Normal file
181
app/Livewire/Forms/RoleTable.php
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
final class RoleTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
public string $tableName = 'role-table';
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if($this->canDelect){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
$header =PowerGrid::header();
|
||||||
|
if($this->canCreate){
|
||||||
|
$header->includeViewOnTop('livewire.header.role');
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
//dd(Role::with('permissions'));
|
||||||
|
return Role::with('permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
$allPermissions = Permission::pluck('name')->sort()->values();
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('name')
|
||||||
|
->add('permissions_list', function (Role $model) use ($allPermissions) {
|
||||||
|
$rolePermissions = $model->permissions->pluck('name')->sort()->values();
|
||||||
|
|
||||||
|
if ($rolePermissions->count() === $allPermissions->count() && $rolePermissions->values()->all() === $allPermissions->values()->all()) {
|
||||||
|
return 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rolePermissions->implode(', ');
|
||||||
|
})
|
||||||
|
->add('created_at_formatted', fn (Role $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make(__('roles.no'), 'id')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('roles.name'), 'name')->sortable()->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => $id.'.'.__('roles.'.$field).':'.$value,
|
||||||
|
'description' => '已經寫入',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::inputText('name')->placeholder(__('roles.name')),
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function actions(Role $row): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$actions[] =Button::add('edit')
|
||||||
|
->slot(__('roles.edit'))
|
||||||
|
->icon('solid-pencil-square')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.role-form', 'openEditRoleModal', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if($this->canDelect){
|
||||||
|
$actions[] =Button::add('delete')
|
||||||
|
->slot(__('roles.delete'))
|
||||||
|
->icon('solid-trash')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.role-form', 'deleteRole', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public function actionRules($row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Hide button edit for ID 1
|
||||||
|
Rule::button('edit')
|
||||||
|
->when(fn($row) => $row->id === 1)
|
||||||
|
->hide(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
125
app/Livewire/Forms/RoomForm.php
Normal file
125
app/Livewire/Forms/RoomForm.php
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Enums\RoomType;
|
||||||
|
|
||||||
|
class RoomForm extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal', 'deleteRoom'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
public ?int $roomId = null;
|
||||||
|
|
||||||
|
public array $typeOptions =[];
|
||||||
|
|
||||||
|
public array $fields = [
|
||||||
|
'floor' =>'',
|
||||||
|
'type' =>'',
|
||||||
|
'name' =>''
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->typeOptions = collect(RoomType::cases())->map(fn ($type) => [
|
||||||
|
'name' => $type->labels(),
|
||||||
|
'value' => $type->value,
|
||||||
|
])->toArray();
|
||||||
|
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canEdit = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canDelect = Auth::user()?->can('room-delete') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal($id = null)
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$room = Room::findOrFail($id);
|
||||||
|
$this->roomId = $room->id;
|
||||||
|
$this->fields = $room->only(array_keys($this->fields));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
$description ="無權修改";
|
||||||
|
if ($this->roomId) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$room = Room::findOrFail($this->roomId);
|
||||||
|
$room->update($this->fields);
|
||||||
|
$description='分店已更新';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($this->canCreate) {
|
||||||
|
$room = Room::create([
|
||||||
|
'floor' => $this->fields['floor'],
|
||||||
|
'type' => $this->fields['type'],
|
||||||
|
'name' => $this->fields['name'],
|
||||||
|
]);
|
||||||
|
$description='分店已新增';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => $description,
|
||||||
|
]);
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
$this->dispatch('pg:eventRefresh-room-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteBranch($id)
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
Room::findOrFail($id)->delete();
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '分店已刪除',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch('pg:eventRefresh-room-table');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFields()
|
||||||
|
{
|
||||||
|
foreach ($this->fields as $key => $value) {
|
||||||
|
if ($key == 'enable') {
|
||||||
|
$this->fields[$key] = true;
|
||||||
|
} else {
|
||||||
|
$this->fields[$key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->branchId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.room-form');
|
||||||
|
}
|
||||||
|
}
|
47
app/Livewire/Forms/RoomGrid.php
Normal file
47
app/Livewire/Forms/RoomGrid.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Enums\RoomType;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
|
||||||
|
class RoomGrid extends Component
|
||||||
|
{
|
||||||
|
protected $listeners = ['openModal','closeModal'];//,'refreshRooms' => '$refresh'
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
public int $branch_id = 0;
|
||||||
|
public $branchName="";
|
||||||
|
public array $roomTypes;
|
||||||
|
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->roomTypes = ['all' => '全部'] + collect(RoomType::cases())->mapWithKeys(fn($e) => [$e->value => $e->labels()])->toArray();
|
||||||
|
}
|
||||||
|
public function openModal($branch_id = null)
|
||||||
|
{
|
||||||
|
$this->branch_id = $branch_id;
|
||||||
|
$branch = Branch::find($branch_id);
|
||||||
|
$this->branchName = Branch::find($branch_id)?->name ?? '';
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
public function closeModal(){
|
||||||
|
$this->showModal = false;
|
||||||
|
$this->branch_id = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$rooms = Room::where('branch_id', $this->branch_id)->get();
|
||||||
|
// 取得樓層
|
||||||
|
$floors = $rooms->pluck('floor')->unique()->sort()->values()->toArray();
|
||||||
|
|
||||||
|
return view('livewire.forms.room-grid',['rooms' =>$rooms,'floors' =>$floors]);
|
||||||
|
}
|
||||||
|
}
|
43
app/Livewire/Forms/RoomGridForm.php
Normal file
43
app/Livewire/Forms/RoomGridForm.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Enums\RoomType;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RoomGridForm extends Component
|
||||||
|
{
|
||||||
|
public $branchName="";
|
||||||
|
public array $roomTypes;
|
||||||
|
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->roomTypes = ['all' => '全部'] + collect(RoomType::cases())->mapWithKeys(fn($e) => [$e->value => $e->labels()])->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$branch = Branch::first();
|
||||||
|
$this->branchName = $branch->name ?? '';
|
||||||
|
|
||||||
|
$rooms = collect(); // 預設為空集合
|
||||||
|
$floors = [];
|
||||||
|
|
||||||
|
if ($branch) {
|
||||||
|
$rooms = Room::where('branch_id', $branch->id)->get();
|
||||||
|
$floors = $rooms->pluck('floor')->unique()->sort()->values()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('livewire.forms.room-grid-form', [
|
||||||
|
'rooms' => $rooms,
|
||||||
|
'floors' => $floors,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
82
app/Livewire/Forms/RoomStatusLogTable.php
Normal file
82
app/Livewire/Forms/RoomStatusLogTable.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\RoomStatusLog;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
|
||||||
|
final class RoomStatusLogTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
public string $tableName = 'room-status-log-table';
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
config(['livewire-powergrid.filter' => 'outside']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->withoutLoading()
|
||||||
|
->showToggleColumns();
|
||||||
|
$header->includeViewOnTop('livewire.header.admin.room-status-log');
|
||||||
|
|
||||||
|
$actions[]=$header;
|
||||||
|
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function datasource(): Builder
|
||||||
|
{
|
||||||
|
return RoomStatusLog::query()->latest();;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('room_name', function (RoomStatusLog $model) {
|
||||||
|
return $model->room?->type->labelPowergridFilter().$model->room?->name;
|
||||||
|
})
|
||||||
|
->add('user_name', function (RoomStatusLog $model){
|
||||||
|
return $model->user?->name;
|
||||||
|
})
|
||||||
|
->add('status_str',function (RoomStatusLog $model){
|
||||||
|
return $model->status->labelPowergridFilter();
|
||||||
|
})
|
||||||
|
->add('message')
|
||||||
|
->add('created_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make(__('room-status-log.id'), 'id');
|
||||||
|
$column[]=Column::make(__('room-status-log.room'), 'room_name');
|
||||||
|
$column[]=Column::make(__('room-status-log.user'), 'user_name');
|
||||||
|
$column[]=Column::make(__('room-status-log.status'), 'status_str');
|
||||||
|
$column[]=Column::make(__('room-status-log.message'), 'message');
|
||||||
|
$column[]=Column::make(__('room-status-log.created_at'), 'created_at');
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
245
app/Livewire/Forms/RoomTable.php
Normal file
245
app/Livewire/Forms/RoomTable.php
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Enums\RoomType;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
final class RoomTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WithExport, WireUiActions;
|
||||||
|
public string $tableName = 'room-table';
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDownload;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
config(['livewire-powergrid.filter' => 'outside']);
|
||||||
|
//權限設定
|
||||||
|
$this->canCreate = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canEdit = Auth::user()?->can('room-edit') ?? false;
|
||||||
|
$this->canDownload=Auth::user()?->can('room-delete') ?? false;
|
||||||
|
$this->canDelect = Auth::user()?->can('room-delete') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): array
|
||||||
|
{
|
||||||
|
if($this->canDownload || $this->canDelect){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
if($this->canDownload){
|
||||||
|
$actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
}
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->withoutLoading()
|
||||||
|
->showToggleColumns();
|
||||||
|
if($this->canCreate){
|
||||||
|
$header->includeViewOnTop('livewire.header.admin.room');
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return Room::query();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('floor')
|
||||||
|
->add('type_str',function(Room $model){
|
||||||
|
if ($this->canEdit) {
|
||||||
|
return Blade::render(
|
||||||
|
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
|
||||||
|
[
|
||||||
|
'options' => RoomType::options(),
|
||||||
|
'modelId' => intval($model->id),
|
||||||
|
'fieldName'=>'type',
|
||||||
|
'selected' => $model->type->value
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $model->type->labelPowergridFilter();
|
||||||
|
})
|
||||||
|
->add('name')
|
||||||
|
->add('internal_ip')
|
||||||
|
->add('port')
|
||||||
|
->add('is_online', fn ($model) => $model->is_online===true ? '在線' : '斷線')
|
||||||
|
->add('status_str',function (Room $model){
|
||||||
|
return $model->status->labelPowergridFilter();
|
||||||
|
})
|
||||||
|
->add('str_started_at', fn (Room $model) => $model->str_started_at())
|
||||||
|
->add('str_ended_at', fn (Room $model) => $model->str_ended_at())
|
||||||
|
->add('created_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make(__('rooms.id'), 'id');
|
||||||
|
$column[]=Column::make(__('rooms.floor'), 'floor')->sortable()->searchable()->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'floor',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
);
|
||||||
|
$column[]=Column::make(__('rooms.type'), 'type_str','room.type')->sortable()->searchable();
|
||||||
|
$column[]=Column::make(__('rooms.name'), 'name')->sortable()->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'name',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
);
|
||||||
|
$column[]=Column::make(__('rooms.internal_ip'), 'internal_ip')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('rooms.port'), 'port')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('rooms.isOnline'), 'is_online')->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('rooms.status'), 'status_str','room.status')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('rooms.started_at'), 'str_started_at', 'started_at')->sortable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('rooms.ended_at'), 'str_ended_at', 'ended_at')->sortable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('rooms.created_at'), 'created_at')->sortable()->searchable();
|
||||||
|
$column[]=Column::action('Action');
|
||||||
|
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::datetimepicker('started_at'),
|
||||||
|
Filter::datetimepicker('ended_at'),
|
||||||
|
Filter::boolean('is_online')
|
||||||
|
->label('在線', '斷線'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('bulkDelete.{tableName}')]
|
||||||
|
public function bulkDelete(): void
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
|
||||||
|
if($this->checkboxValues){
|
||||||
|
Branch::destroy($this->checkboxValues);
|
||||||
|
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[On('categoryChanged')]
|
||||||
|
public function categoryChanged($value,$fieldName, $modelId): void
|
||||||
|
{
|
||||||
|
if ($fieldName == 'type' && $this->canEdit) {
|
||||||
|
$this->noUpdated($modelId,$fieldName,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[On('onUpdatedEditable')]
|
||||||
|
public function onUpdatedEditable($id, $field, $value): void
|
||||||
|
{
|
||||||
|
if($field === 'floor' && $this->canEdit){
|
||||||
|
if (!is_numeric($value)) {
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'error',
|
||||||
|
'title' => '無效輸入',
|
||||||
|
'description' => '樓層必須是數字',
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->noUpdated($id,$field,$value);
|
||||||
|
}else if ($field === 'name' && $this->canEdit) {
|
||||||
|
$this->noUpdated($id,$field,$value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function noUpdated($id,$field,$value){
|
||||||
|
dd($id,$field,$value);
|
||||||
|
$room = Room::find($id);
|
||||||
|
if ($room) {
|
||||||
|
$room->{$field} = $value;
|
||||||
|
$room->save(); // 明確觸發 saving
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => $id.'.'.__('room.'.$field).':'.$value,
|
||||||
|
'description' => '已經寫入',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actions(Room $row): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$actions[] =Button::add('edit')
|
||||||
|
->slot(__('rooms.edit'))
|
||||||
|
->icon('solid-pencil-square')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.room-form', 'openModal', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if($this->canDelect){
|
||||||
|
$actions[] =Button::add('delete')
|
||||||
|
->slot(__('rooms.delete'))
|
||||||
|
->icon('solid-trash')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.room-form', 'deleteRoom', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if ($row->type->value === 'pc') {
|
||||||
|
$actions[] = Button::add('room-settings')
|
||||||
|
->slot('包廂設定')
|
||||||
|
->icon('solid-cog')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.modals.room-detail-modal', 'openModal', ['roomId' => $row->id]);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public function actionRules($row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Hide button edit for ID 1
|
||||||
|
Rule::button('edit')
|
||||||
|
->when(fn($row) => $row->id === 1)
|
||||||
|
->hide(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
154
app/Livewire/Forms/SongForm.php
Normal file
154
app/Livewire/Forms/SongForm.php
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
use App\Enums\SongLanguageType;
|
||||||
|
use App\Enums\SongSituation;
|
||||||
|
use App\Models\Song;
|
||||||
|
use App\Models\SongCategory;
|
||||||
|
|
||||||
|
class SongForm extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal', 'deleteSong'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
|
||||||
|
public $songCategories =[];
|
||||||
|
public $songLanguageType =[];
|
||||||
|
public $songSituation =[];
|
||||||
|
public $selectedCategories=[];
|
||||||
|
public $selectedArtists=[];
|
||||||
|
|
||||||
|
public ?int $songId = null;
|
||||||
|
|
||||||
|
public array $fields = [
|
||||||
|
'id' =>'',
|
||||||
|
'name' => '',
|
||||||
|
'adddate' => '',
|
||||||
|
'filename' => '',
|
||||||
|
'language_type' => '',
|
||||||
|
|
||||||
|
'db_change' => '',
|
||||||
|
'vocal' => '',
|
||||||
|
'situation' => '',
|
||||||
|
'copyright01' => '',
|
||||||
|
'copyright02' => '',
|
||||||
|
'note01' => '',
|
||||||
|
'note02' => '',
|
||||||
|
'note03' => '',
|
||||||
|
'note04' => '',
|
||||||
|
'enable' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
//protected $rules = [
|
||||||
|
// 'name' => 'required|string|max:255',
|
||||||
|
//
|
||||||
|
//];
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->songCategories = SongCategory::all();
|
||||||
|
$this->songLanguageType = collect(SongLanguageType::cases())->map(fn ($languageType) => [
|
||||||
|
'name' => $languageType->labels(),
|
||||||
|
'value' => $languageType->value,
|
||||||
|
])->toArray();
|
||||||
|
$this->songSituation = collect(SongSituation::cases())->map(fn ($situation) => [
|
||||||
|
'name' => $situation->labels(),
|
||||||
|
'value' => $situation->value,
|
||||||
|
])->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal($id = null)
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
if ($id) {
|
||||||
|
$song = Song::findOrFail($id);
|
||||||
|
$this->songId = $song->id;
|
||||||
|
$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->showModal = true;
|
||||||
|
}
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
//$this->validate();
|
||||||
|
|
||||||
|
if ($this->songId) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$song = Song::findOrFail($this->songId);
|
||||||
|
$song->update($this->fields);
|
||||||
|
// ⭐ 同步多對多關聯
|
||||||
|
$song->artists()->sync($this->selectedArtists ?? []);
|
||||||
|
$song->categories()->sync($this->selectedCategories ?? []);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '歌曲已更新',
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($this->canCreate) {
|
||||||
|
$song = Song::create($this->fields);
|
||||||
|
// ⭐ 同步多對多關聯
|
||||||
|
$song->artists()->sync($this->selectedArtists ?? []);
|
||||||
|
$song->categories()->sync($this->selectedCategories ?? []);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '歌曲已新增',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
$this->dispatch('pg:eventRefresh-song-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteSong($id)
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
Song::findOrFail($id)->delete();
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '歌曲已刪除',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFields()
|
||||||
|
{
|
||||||
|
foreach ($this->fields as $key => $value) {
|
||||||
|
$this->fields[$key] = is_bool($value) ? false : '';
|
||||||
|
}
|
||||||
|
$this->songId = null;
|
||||||
|
}
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.song-form');
|
||||||
|
}
|
||||||
|
}
|
114
app/Livewire/Forms/SongImportData.php
Normal file
114
app/Livewire/Forms/SongImportData.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
use App\Jobs\ImportJob;
|
||||||
|
|
||||||
|
|
||||||
|
class SongImportData extends Component
|
||||||
|
{
|
||||||
|
use WithFileUploads, WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
|
||||||
|
public $file;
|
||||||
|
public string $maxUploadSize;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->canCreate = Auth::user()?->can('song-edit') ?? false;
|
||||||
|
$this->maxUploadSize = $this->getMaxUploadSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal()
|
||||||
|
{
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->deleteTmpFile(); // 關閉 modal 時刪除暫存檔案
|
||||||
|
$this->reset(['file']);
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import()
|
||||||
|
{
|
||||||
|
// 檢查檔案是否有上傳
|
||||||
|
$this->validate([
|
||||||
|
'file' => 'required|file|mimes:csv,xlsx,xls'
|
||||||
|
]);
|
||||||
|
if ($this->canCreate) {
|
||||||
|
// 儲存檔案至 storage
|
||||||
|
$path = $this->file->storeAs('imports', uniqid() . '_' . $this->file->getClientOriginalName());
|
||||||
|
|
||||||
|
// 丟到 queue 執行
|
||||||
|
ImportJob::dispatch($path,'Song');
|
||||||
|
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'info',
|
||||||
|
'title' => $this->file->getClientOriginalName(),
|
||||||
|
'description' => '已排入背景匯入作業,請稍候查看結果',
|
||||||
|
]);
|
||||||
|
$this->deleteTmpFile(); // 匯入後也順便刪除 tmp 檔
|
||||||
|
$this->reset(['file']);
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected function deleteTmpFile()
|
||||||
|
{
|
||||||
|
if($this->file!=null){
|
||||||
|
$Path = $this->file->getRealPath();
|
||||||
|
if ($Path && File::exists($Path)) {
|
||||||
|
File::delete($Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMaxUploadSize(): string
|
||||||
|
{
|
||||||
|
$uploadMax = $this->convertPHPSizeToBytes(ini_get('upload_max_filesize'));
|
||||||
|
$postMax = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
|
||||||
|
$max = min($uploadMax, $postMax);
|
||||||
|
return $this->humanFileSize($max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertPHPSizeToBytes(string $s): int
|
||||||
|
{
|
||||||
|
$s = trim($s);
|
||||||
|
$unit = strtolower($s[strlen($s) - 1]);
|
||||||
|
$bytes = (int) $s;
|
||||||
|
switch ($unit) {
|
||||||
|
case 'g':
|
||||||
|
$bytes *= 1024;
|
||||||
|
case 'm':
|
||||||
|
$bytes *= 1024;
|
||||||
|
case 'k':
|
||||||
|
$bytes *= 1024;
|
||||||
|
}
|
||||||
|
return $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function humanFileSize(int $bytes, int $decimals = 2): string
|
||||||
|
{
|
||||||
|
$sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$factor = floor((strlen((string) $bytes) - 1) / 3);
|
||||||
|
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sizes[$factor];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.song-import-data');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
291
app/Livewire/Forms/SongTable.php
Normal file
291
app/Livewire/Forms/SongTable.php
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\Song;
|
||||||
|
use App\Enums\SongLanguageType;
|
||||||
|
use App\Enums\SongSituation;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Rule;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
final class SongTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WithExport, WireUiActions;
|
||||||
|
|
||||||
|
public string $tableName = 'song-table';
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDownload;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
/* public bool $deferLoading = true;
|
||||||
|
|
||||||
|
public string $loadingComponent = 'components.my-custom-loading'; */
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if($this->canDownload || $this->canDelect){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
if($this->canDownload){
|
||||||
|
$actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
}
|
||||||
|
$header = PowerGrid::header()->showSoftDeletes()->showToggleColumns();
|
||||||
|
if($this->canCreate){
|
||||||
|
$header->includeViewOnTop('livewire.header.song');
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return Song::query()->with([
|
||||||
|
'artists:id,name'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('name')
|
||||||
|
->add('simplified')
|
||||||
|
->add('phonetic_abbr')
|
||||||
|
->add('pinyin_abbr')
|
||||||
|
->add('strokes_abbr')
|
||||||
|
->add('filename')
|
||||||
|
->add('adddate_formatted', fn (Song $model) => Carbon::parse($model->adddate)->format('Y-m-d'))
|
||||||
|
->add('song_artists' ,fn(Song $model)=> $model->str_artists())
|
||||||
|
//->add('language_type_str', fn (Song $model) => SongLanguageType::from($model->language_type->value)->labels())
|
||||||
|
->add('language_type_str', function (Song $model) {
|
||||||
|
return Blade::render(
|
||||||
|
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
|
||||||
|
[
|
||||||
|
'options' => SongLanguageType::options(),
|
||||||
|
'modelId' => intval($model->id),
|
||||||
|
'fieldName'=>'language_type',
|
||||||
|
'selected' => $model->language_type->value
|
||||||
|
]);
|
||||||
|
} )
|
||||||
|
->add('song_categories', fn(Song $model) => $model->str_categories())
|
||||||
|
->add('db_change')
|
||||||
|
->add('vocal')
|
||||||
|
//->add('situation_str', fn (Song $model) => SongSituation::from($model->situation->value)->labels())
|
||||||
|
->add('situation_str', function (Song $model){
|
||||||
|
return Blade::render(
|
||||||
|
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
|
||||||
|
[
|
||||||
|
'options' => SongSituation::options(),
|
||||||
|
'modelId' => intval($model->id),
|
||||||
|
'fieldName'=>'situation',
|
||||||
|
'selected' => $model->situation->value
|
||||||
|
]);
|
||||||
|
} )
|
||||||
|
->add('copyright01')
|
||||||
|
->add('copyright02')
|
||||||
|
->add('note01')
|
||||||
|
->add('note02')
|
||||||
|
->add('note03')
|
||||||
|
->add('note04')
|
||||||
|
->add('enable')
|
||||||
|
->add('created_at_formatted', fn (Song $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
$column=[];
|
||||||
|
$column[]=Column::make(__('songs.id'), 'id')
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'id', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.name'), 'name')->sortable()->searchable()
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'name', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.simplified'), 'simplified')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('songs.name.phinetic'), 'phonetic_abbr')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('songs.name.pinyin'), 'pinyin_abbr')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('songs.name.strokes'), 'strokes_abbr')->sortable()->searchable()->hidden(true, false);
|
||||||
|
$column[]=Column::make(__('songs.filename'), 'filename')->sortable()->searchable()
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'filename', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.adddate'), 'adddate_formatted', 'adddate')->sortable();
|
||||||
|
$column[]=Column::make(__('songs.artists'), 'song_artists');
|
||||||
|
$column[]=Column::make(__('songs.language_type'),'language_type_str', 'songs.language_type')->searchable();
|
||||||
|
$column[]=Column::make(__('songs.categorys'), 'song_categories');
|
||||||
|
$column[]=Column::make('Db change', 'db_change')
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'db_change', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.vocal'), 'vocal')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no');
|
||||||
|
$column[]=Column::make(__('songs.situation'), 'situation_str','songs.situation')->searchable();
|
||||||
|
$column[]=Column::make(__('songs.copyright01'), 'copyright01')->sortable()->searchable()
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'copyright01', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.copyright02'), 'copyright02')->sortable()->searchable()->hidden(true, false)
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'copyright02', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.note01'), 'note01')->sortable()->searchable()
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'note01', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.note02'), 'note02')->sortable()->searchable()->hidden(true, false)
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'note02', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.note03'), 'note03')->sortable()->searchable()->hidden(true, false)
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'note03', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.note04'), 'note04')->sortable()->searchable()->hidden(true, false)
|
||||||
|
->editOnClick(hasPermission: $this->canEdit, dataField: 'note04', fallback: 'N/A', saveOnMouseOut: true);
|
||||||
|
$column[]=Column::make(__('songs.enable'), 'enable')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no');
|
||||||
|
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
|
||||||
|
$column[]=Column::action(__('songs.actions'));
|
||||||
|
return $column;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::number('id')->placeholder(0,9999999),
|
||||||
|
Filter::inputText('name')->placeholder(__('songs.name')),
|
||||||
|
Filter::inputText('phonetic_abbr')->placeholder(__('songs.name.phinetic')),
|
||||||
|
Filter::inputText('pinyin_abbr')->placeholder(__('songs.name.pinyin_abbr')),
|
||||||
|
Filter::number('strokes_abbr'),
|
||||||
|
Filter::inputText('filename')->placeholder(__('songs.filename')),
|
||||||
|
Filter::datepicker('adddate'),
|
||||||
|
Filter::enumSelect('language_type_str','songs.language_type')
|
||||||
|
->datasource(SongLanguageType::cases())
|
||||||
|
->optionLabel('songs.language_type'),
|
||||||
|
Filter::inputText('copyright01')->placeholder(__('songs.copyright01')),
|
||||||
|
Filter::inputText('copyright02')->placeholder(__('songs.copyright02')),
|
||||||
|
Filter::inputText('note01')->placeholder(__('songs.note01')),
|
||||||
|
Filter::inputText('note02')->placeholder(__('songs.note02')),
|
||||||
|
Filter::inputText('note03')->placeholder(__('songs.note03')),
|
||||||
|
Filter::inputText('note04')->placeholder(__('songs.note04')),
|
||||||
|
Filter::boolean('enable')->label('✅', '❌'),
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
#[On('bulkDelete.{tableName}')]
|
||||||
|
public function bulkDelete(): void
|
||||||
|
{
|
||||||
|
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
|
||||||
|
if($this->checkboxValues){
|
||||||
|
foreach ($this->checkboxValues as $id) {
|
||||||
|
$song = Song::find($id);
|
||||||
|
if ($song) {
|
||||||
|
$song->artists()->detach();
|
||||||
|
$song->categories()->detach();
|
||||||
|
$song->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => $id.'.'.__('songs.'.$field).':'.$value,
|
||||||
|
'description' => '已經寫入',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function actions(Song $row): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$actions[]=Button::add('edit')
|
||||||
|
->slot(__('songs.edit'))
|
||||||
|
->icon('solid-pencil-square')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.song-form', 'openModal', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if($this->canDelect){
|
||||||
|
$actions[]=Button::add('delete')
|
||||||
|
->slot(__('songs.delete'))
|
||||||
|
->icon('solid-trash')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.song-form', 'deleteSong', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public function actionRules($row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Hide button edit for ID 1
|
||||||
|
Rule::button('edit')
|
||||||
|
->when(fn($row) => $row->id === 1)
|
||||||
|
->hide(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
153
app/Livewire/Forms/UserForm.php
Normal file
153
app/Livewire/Forms/UserForm.php
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Enums\UserGender;
|
||||||
|
use App\Enums\UserStatus;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class UserForm extends Component
|
||||||
|
{
|
||||||
|
use WireUiActions;
|
||||||
|
|
||||||
|
protected $listeners = ['openModal','closeModal', 'deleteUser'];
|
||||||
|
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public bool $showModal = false;
|
||||||
|
|
||||||
|
public array $genderOptions =[];
|
||||||
|
public array $statusOptions =[];
|
||||||
|
public $rolesOptions = []; // 所有角色清單
|
||||||
|
public $selectedRoles = []; // 表單中選到的權限
|
||||||
|
public ?int $userId = null;
|
||||||
|
public array $fields = [
|
||||||
|
'name' =>'',
|
||||||
|
'email' => '',
|
||||||
|
'phone' => '',
|
||||||
|
'birthday' => '',
|
||||||
|
'gender' => 'unset',
|
||||||
|
'status' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
|
'fields.name' => 'required|string|max:255',
|
||||||
|
'fields.email' => 'required|string|email|max:255',
|
||||||
|
'fields.phone' => 'nullable|regex:/^09\d{8}$/',
|
||||||
|
'fields.birthday' =>'nullable|date',
|
||||||
|
'fields.gender' => 'required|in:male,female,other,unset',
|
||||||
|
'fields.status' => 'required|integer|in:0,1,2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->fields['birthday'] = now()->toDateString();
|
||||||
|
$this->genderOptions = collect(UserGender::cases())->map(fn ($gender) => [
|
||||||
|
'name' => $gender->labels(),
|
||||||
|
'value' => $gender->value,
|
||||||
|
])->toArray();
|
||||||
|
$this->statusOptions = collect(UserStatus::cases())->map(fn ($status) => [
|
||||||
|
'name' => $status->labels(),
|
||||||
|
'value' => $status->value,
|
||||||
|
])->toArray();
|
||||||
|
$this->rolesOptions = Role::all();
|
||||||
|
$this->canCreate = Auth::user()?->can('user-edit') ?? false;
|
||||||
|
$this->canEdit = Auth::user()?->can('user-edit') ?? false;
|
||||||
|
$this->canDelect = Auth::user()?->can('user-delete') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openModal($id = null)
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
if($id){
|
||||||
|
$obj = User::findOrFail($id);
|
||||||
|
$this->userId = $obj->id;
|
||||||
|
$this->fields = $obj->only(array_keys($this->fields));
|
||||||
|
$this->selectedRoles = $obj->roles()->pluck('id')->toArray();
|
||||||
|
}
|
||||||
|
$this->showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeModal()
|
||||||
|
{
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
//$this->validate();
|
||||||
|
|
||||||
|
if ($this->userId) {
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$user = User::findOrFail($this->userId);
|
||||||
|
$user->update($this->fields);
|
||||||
|
$user->syncRoles($this->selectedRoles);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '使用者已更新',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($this->canCreate) {
|
||||||
|
$user = User::create($this->fields);
|
||||||
|
$user->syncRoles($this->selectedRoles);
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '使用者已新增',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resetFields();
|
||||||
|
$this->showModal = false;
|
||||||
|
$this->dispatch('pg:eventRefresh-user-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteUser($id)
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
User::findOrFail($id)->delete();
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => '成功',
|
||||||
|
'description' => '使用者已刪除',
|
||||||
|
]);
|
||||||
|
$this->dispatch('pg:eventRefresh-user-table');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetFields()
|
||||||
|
{
|
||||||
|
foreach ($this->fields as $key => $value) {
|
||||||
|
if ($key == 'gender') {
|
||||||
|
$this->fields[$key] = 'unset';
|
||||||
|
} elseif ($key == 'status') {
|
||||||
|
$this->fields[$key] = 0;
|
||||||
|
} elseif ($key == 'birthday') {
|
||||||
|
$this->fields[$key] = now()->toDateString();
|
||||||
|
} else {
|
||||||
|
$this->fields[$key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->userId = null;
|
||||||
|
$this->selectedRoles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.forms.user-form');
|
||||||
|
}
|
||||||
|
}
|
265
app/Livewire/Forms/UserTable.php
Normal file
265
app/Livewire/Forms/UserTable.php
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Enums\UserGender;
|
||||||
|
use App\Enums\UserStatus;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Button;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Column;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||||
|
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||||
|
use PowerComponents\LivewirePowerGrid\Facades\Rule;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use WireUi\Traits\WireUiActions;
|
||||||
|
|
||||||
|
final class UserTable extends PowerGridComponent
|
||||||
|
{
|
||||||
|
use WithExport, WireUiActions;
|
||||||
|
|
||||||
|
public string $tableName = 'user-table';
|
||||||
|
|
||||||
|
public bool $showFilters = false;
|
||||||
|
public bool $canCreate;
|
||||||
|
public bool $canEdit;
|
||||||
|
public bool $canDownload;
|
||||||
|
public bool $canDelect;
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
if($this->canDownload || $this->canDelect){
|
||||||
|
$this->showCheckBox();
|
||||||
|
}
|
||||||
|
$actions = [];
|
||||||
|
$actions[] =PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||||
|
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||||
|
$header = PowerGrid::header()
|
||||||
|
->showToggleColumns();
|
||||||
|
if($this->canCreate){
|
||||||
|
$header->includeViewOnTop('livewire.header.user');
|
||||||
|
}
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return User::query();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relationSearch(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): PowerGridFields
|
||||||
|
{
|
||||||
|
|
||||||
|
return PowerGrid::fields()
|
||||||
|
->add('id')
|
||||||
|
->add('name')
|
||||||
|
->add('email')
|
||||||
|
->add('phone')
|
||||||
|
->add('birthday_formatted',fn (User $model) => Carbon::parse($model->birthday)->format('Y-m-d'))
|
||||||
|
->add('gender_str', function (User $model) {
|
||||||
|
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('created_at_formatted', fn (User $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function columns(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Column::make('ID', 'id'),
|
||||||
|
Column::make(__('users.name'), 'name')
|
||||||
|
->sortable()
|
||||||
|
->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'name',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
),
|
||||||
|
Column::make('Email', 'email')
|
||||||
|
->sortable()
|
||||||
|
->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'email',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
),
|
||||||
|
Column::make(__('users.phone'), 'phone')
|
||||||
|
->sortable()
|
||||||
|
->searchable()
|
||||||
|
->editOnClick(
|
||||||
|
hasPermission: $this->canEdit,
|
||||||
|
dataField: 'phone',
|
||||||
|
fallback: 'N/A',
|
||||||
|
saveOnMouseOut: true
|
||||||
|
),
|
||||||
|
|
||||||
|
Column::make(__('users.gender'), 'gender_str','users.gender'),
|
||||||
|
Column::make(__('users.birthday'), 'birthday_formatted')->sortable()->searchable(),
|
||||||
|
Column::make(__('users.status'), 'status_str','users.status'),
|
||||||
|
Column::make(__('users.role'), 'roles'),
|
||||||
|
Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(),
|
||||||
|
Column::action('操作')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
#[On('bulkDelete.{tableName}')]
|
||||||
|
public function bulkDelete(): void
|
||||||
|
{
|
||||||
|
if ($this->canDelect) {
|
||||||
|
$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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[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){
|
||||||
|
$user = User::find($id);
|
||||||
|
if ($user) {
|
||||||
|
$user->{$field} = $value;
|
||||||
|
$user->save(); // 明確觸發 saving
|
||||||
|
}
|
||||||
|
$this->notification()->send([
|
||||||
|
'icon' => 'success',
|
||||||
|
'title' => $id.'.'.__('users.'.$field).':'.$value,
|
||||||
|
'description' => '已經寫入',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Filter::inputText('name')->placeholder(__('users.name')),
|
||||||
|
Filter::inputText('email')->placeholder('Email'),
|
||||||
|
Filter::inputText('phone')->placeholder(__('users.phone')),
|
||||||
|
Filter::enumSelect('gender_str','users.gender')
|
||||||
|
->datasource(UserGender::cases())
|
||||||
|
->optionLabel('users.gender'),
|
||||||
|
Filter::datepicker('birthday'),
|
||||||
|
Filter::enumSelect('status_str', 'users.status')
|
||||||
|
->datasource(UserStatus::cases())
|
||||||
|
->optionLabel('users.status'),
|
||||||
|
Filter::datetimepicker('created_at'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actions(User $row): array
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
if ($this->canEdit) {
|
||||||
|
$actions[]=Button::add('edit')
|
||||||
|
->slot(__('users.edit'))
|
||||||
|
->icon('solid-pencil-square')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.user-form', 'openModal', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
if($this->canDelect){
|
||||||
|
$actions[]=Button::add('delete')
|
||||||
|
->slot(__('users.delete'))
|
||||||
|
->icon('solid-trash')
|
||||||
|
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||||
|
->dispatchTo('forms.user-form', 'deleteUser', ['id' => $row->id]);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* public function actionRules($row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Hide button edit for ID 1
|
||||||
|
Rule::button('edit')
|
||||||
|
->when(fn($row) => $row->id === 1)
|
||||||
|
->hide(),
|
||||||
|
];
|
||||||
|
} */
|
||||||
|
|
||||||
|
}
|
57
app/Models/Artist.php
Normal file
57
app/Models/Artist.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
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, LogsModelActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'category',
|
||||||
|
'name',
|
||||||
|
'simplified',
|
||||||
|
'phonetic_abbr',
|
||||||
|
'pinyin_abbr',
|
||||||
|
'strokes_abbr',
|
||||||
|
'enable',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'category' => \App\Enums\ArtistCategory::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function songs() {
|
||||||
|
return $this->belongsToMany(Song::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
// 無論是 creating 或 updating,都執行這段共用的邏輯
|
||||||
|
static::saving(function (Artist $artist) {
|
||||||
|
$simplified=ChineseNameConverter::convertToSimplified($artist->name);// 繁體轉簡體
|
||||||
|
$artist->simplified = $simplified;
|
||||||
|
$artist->phonetic_abbr = ChineseNameConverter::getKTVZhuyinAbbr($simplified);// 注音符號
|
||||||
|
$artist->pinyin_abbr=ChineseNameConverter::getKTVPinyinAbbr($simplified);// 拼音首字母
|
||||||
|
|
||||||
|
$chars = preg_split('//u', $artist->name, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$firstChar = $chars[0] ?? null;
|
||||||
|
$artist->strokes_abbr=( $firstChar && preg_match('/\p{Han}/u', $firstChar) ) ? ChineseStrokesConverter::getStrokes($firstChar) : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleting(function (Artist $artist) {
|
||||||
|
// 解除與歌曲的多對多關聯
|
||||||
|
$artist->songs()->detach();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
app/Models/Branch.php
Normal file
38
app/Models/Branch.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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, LogsModelActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'external_ip',
|
||||||
|
'enable',
|
||||||
|
];
|
||||||
|
protected $casts = [
|
||||||
|
'name' => 'string',
|
||||||
|
'external_ip'=> 'string',
|
||||||
|
'enable' => 'boolean',
|
||||||
|
];
|
||||||
|
public function rooms() {
|
||||||
|
return $this->hasMany(Room::class);
|
||||||
|
}
|
||||||
|
public function songs(){
|
||||||
|
return $this->belongsToMany(Song::class)
|
||||||
|
->withPivot('counts')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
static::deleting(function (Branch $branch) {
|
||||||
|
$branch->rooms()->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
app/Models/MachineStatus.php
Normal file
32
app/Models/MachineStatus.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="MachineStatus",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||||
|
* @OA\Property(property="hostname", type="string", example="PC101"),
|
||||||
|
* @OA\Property(property="ip", type="string", example="192.168.XX.XX"),
|
||||||
|
* @OA\Property(property="cpu", type="numeric", example="0.00"),
|
||||||
|
* @OA\Property(property="memory", type="numeric", example="25603"),
|
||||||
|
* @OA\Property(property="disk", type="numeric", example="158266.49"),
|
||||||
|
* @OA\Property(property="status", type="string", example="online,error"),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class MachineStatus extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'branch_name',
|
||||||
|
'hostname',
|
||||||
|
'ip',
|
||||||
|
'cpu',
|
||||||
|
'memory',
|
||||||
|
'disk',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
79
app/Models/Room.php
Normal file
79
app/Models/Room.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Traits\LogsModelActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="Room",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="id", type="integer", example=16),
|
||||||
|
* @OA\Property(property="floor", type="integer", example="1"),
|
||||||
|
* @OA\Property(property="type", ref="#/components/schemas/RoomType"),
|
||||||
|
* @OA\Property(property="name", type="string", example="pc102"),
|
||||||
|
* @OA\Property(property="internal_ip", type="string", example="192.168.11.7"),
|
||||||
|
* @OA\Property(property="port", type="int", example="9000"),
|
||||||
|
* @OA\Property(property="status", ref="#/components/schemas/RoomStatus"),
|
||||||
|
* @OA\Property(property="started_at", type="string", format="date-time", example="2025-05-11T16:00:00.000000Z"),
|
||||||
|
* @OA\Property(property="ended_at", type="string", format="date-time", example=null),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Room extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
||||||
|
use HasFactory, LogsModelActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'floor',
|
||||||
|
'type',
|
||||||
|
'name',
|
||||||
|
'internal_ip',
|
||||||
|
'port',
|
||||||
|
'is_online',
|
||||||
|
'status',
|
||||||
|
'started_at',
|
||||||
|
'ended_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'floor' => 'int',
|
||||||
|
'type' => \App\Enums\RoomType::class,
|
||||||
|
'name' => 'string',
|
||||||
|
'internal_ip' =>'string',
|
||||||
|
'port' => 'int',
|
||||||
|
'is_online' => 'boolean',
|
||||||
|
'status' => \App\Enums\RoomStatus::class,
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'ended_at' => 'datetime',
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
public function str_started_at(){
|
||||||
|
$str ="Not Set";
|
||||||
|
if($this->started_at !=null){
|
||||||
|
$str = $this->started_at;
|
||||||
|
}
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function str_ended_at(){
|
||||||
|
$str ="Not Set";
|
||||||
|
if($this->ended_at !=null){
|
||||||
|
$str = $this->ended_at;
|
||||||
|
}
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function branch() {
|
||||||
|
return $this->belongsTo(Branch::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function statusLogs() {
|
||||||
|
return $this->hasMany(RoomStatusLog::class);
|
||||||
|
}
|
||||||
|
}
|
32
app/Models/RoomStatusLog.php
Normal file
32
app/Models/RoomStatusLog.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class RoomStatusLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
|
protected $fillable =
|
||||||
|
[
|
||||||
|
'room_id',
|
||||||
|
'user_id',
|
||||||
|
'status',
|
||||||
|
'message',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'status' => \App\Enums\RoomStatus::class,
|
||||||
|
];
|
||||||
|
public function user(){
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
public function room() {
|
||||||
|
return $this->belongsTo(Room::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
94
app/Models/Song.php
Normal file
94
app/Models/Song.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
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, LogsModelActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'adddate',
|
||||||
|
'filename',
|
||||||
|
'language_type',
|
||||||
|
'db_change',
|
||||||
|
'vocal',
|
||||||
|
'situation',
|
||||||
|
'copyright01',
|
||||||
|
'copyright02',
|
||||||
|
'note01',
|
||||||
|
'note02',
|
||||||
|
'note03',
|
||||||
|
'note04',
|
||||||
|
'enable',
|
||||||
|
'simplified',
|
||||||
|
'phonetic_abbr',
|
||||||
|
'pinyin_abbr',
|
||||||
|
'strokes_abbr',
|
||||||
|
'song_number',
|
||||||
|
'song_counts',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'vocal' => 'boolean',
|
||||||
|
'enable' => 'boolean',
|
||||||
|
'language_type' => \App\Enums\SongLanguageType::class,
|
||||||
|
'situation' => \App\Enums\SongSituation::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function users(){
|
||||||
|
return $this->belongsToMany(User::class, 'user_song')->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function str_artists(){
|
||||||
|
return $this->artists->pluck('name')->implode(', ');
|
||||||
|
}
|
||||||
|
public function artists(){
|
||||||
|
return $this->belongsToMany(Artist::class);
|
||||||
|
}
|
||||||
|
public function str_categories(){
|
||||||
|
return $this->categories->pluck('name')->implode(', ');
|
||||||
|
}
|
||||||
|
public function categories(){
|
||||||
|
return $this->belongsToMany(SongCategory::class);
|
||||||
|
}
|
||||||
|
public function branches(){
|
||||||
|
return $this->belongsToMany(Branch::class)
|
||||||
|
->withPivot('counts')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
// 無論是 creating 或 updating,都執行這段共用的邏輯
|
||||||
|
static::saving(function (Song $song) {
|
||||||
|
$simplified=ChineseNameConverter::convertToSimplified($song->name);// 繁體轉簡體
|
||||||
|
$song->simplified = $simplified;
|
||||||
|
$song->phonetic_abbr = ChineseNameConverter::getKTVZhuyinAbbr($simplified);// 注音符號
|
||||||
|
$song->pinyin_abbr=ChineseNameConverter::getKTVPinyinAbbr($simplified);// 拼音首字母
|
||||||
|
|
||||||
|
$chars = preg_split('//u', $song->name, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$firstChar = $chars[0] ?? null;
|
||||||
|
|
||||||
|
$song->strokes_abbr=($firstChar && preg_match('/\p{Han}/u', $firstChar)) ? ChineseStrokesConverter::getStrokes($firstChar) : 0;
|
||||||
|
$song->song_number = mb_strlen($song->name, 'UTF-8');
|
||||||
|
});
|
||||||
|
static::deleting(function (Song $song) {
|
||||||
|
// Detach 關聯資料
|
||||||
|
$song->artists()->detach();
|
||||||
|
$song->categories()->detach();
|
||||||
|
$song->branches()->detach();
|
||||||
|
$song->users()->detach();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
21
app/Models/SongCategory.php
Normal file
21
app/Models/SongCategory.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class SongCategory extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\SongCategoryFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function songs() {
|
||||||
|
return $this->belongsToMany(Song::class);
|
||||||
|
}
|
||||||
|
}
|
42
app/Models/SongLibraryCache.php
Normal file
42
app/Models/SongLibraryCache.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class SongLibraryCache extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'song_library_cache';
|
||||||
|
protected $primaryKey = 'song_id';
|
||||||
|
public $incrementing = true;
|
||||||
|
protected $keyType = 'int';
|
||||||
|
const CREATED_AT = null;
|
||||||
|
const UPDATED_AT = 'updated_at';
|
||||||
|
|
||||||
|
// 可寫入的欄位(可依需要擴充)
|
||||||
|
protected $fillable = [
|
||||||
|
'song_name',
|
||||||
|
'song_simplified',
|
||||||
|
'phonetic_abbr',
|
||||||
|
'pinyin_abbr',
|
||||||
|
'strokes_abbr',
|
||||||
|
'song_number',
|
||||||
|
'artistA',
|
||||||
|
'artistB',
|
||||||
|
'artistA_simplified',
|
||||||
|
'artistB_simplified',
|
||||||
|
'artistA_category',
|
||||||
|
'artistB_category',
|
||||||
|
'artist_category',
|
||||||
|
'song_filename',
|
||||||
|
'song_category',
|
||||||
|
'language_name',
|
||||||
|
'add_date',
|
||||||
|
'situation',
|
||||||
|
'vocal',
|
||||||
|
'db_change',
|
||||||
|
'song_counts',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
77
app/Models/User.php
Normal file
77
app/Models/User.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
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;
|
||||||
|
use Spatie\Activitylog\Traits\CausesActivity;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="User",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="name", type="string", example="John Doe"),
|
||||||
|
* @OA\Property(property="email", type="string", example="john@example.com"),
|
||||||
|
* @OA\Property(property="phone", type="string", example="0900000000"),
|
||||||
|
* @OA\Property(property="birthday", type="string", format="date-time", example="2025-05-11T16:00:00.000000Z"),
|
||||||
|
* @OA\Property(property="gender", ref="#/components/schemas/UserGender"),
|
||||||
|
* @OA\Property(property="status", ref="#/components/schemas/UserStatus"),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
|
use HasApiTokens, HasFactory, Notifiable, HasRoles, LogsModelActivity,CausesActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'birthday',
|
||||||
|
'gender',
|
||||||
|
'status',
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be hidden for serialization.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
'api_plain_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
'birthday' => 'date',
|
||||||
|
'gender' => \App\Enums\UserGender::class,
|
||||||
|
'status' => \App\Enums\UserStatus::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function songs() {
|
||||||
|
return $this->belongsToMany(Song::class, 'user_song')->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
59
app/Observers/RoomObserver.php
Normal file
59
app/Observers/RoomObserver.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Models\RoomStatusLog;
|
||||||
|
|
||||||
|
class RoomObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the Room "created" event.
|
||||||
|
*/
|
||||||
|
public function created(Room $room): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Room "updated" event.
|
||||||
|
*/
|
||||||
|
public function updated(Room $room): void
|
||||||
|
{
|
||||||
|
// 檢查是否有變更狀態
|
||||||
|
if ($room->wasChanged('status')) {
|
||||||
|
RoomStatusLog::create([
|
||||||
|
'room_id' => $room->id,
|
||||||
|
'user_id' => Auth::id(), // 若是 console 或系統自動操作可能為 null
|
||||||
|
'status' => $room->status,
|
||||||
|
'message' => 'started_at:'.$room->started_at.',ended_at:'.$room->ended_at,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Room "deleted" event.
|
||||||
|
*/
|
||||||
|
public function deleted(Room $room): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Room "restored" event.
|
||||||
|
*/
|
||||||
|
public function restored(Room $room): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Room "force deleted" event.
|
||||||
|
*/
|
||||||
|
public function forceDeleted(Room $room): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
27
app/Providers/AppServiceProvider.php
Normal file
27
app/Providers/AppServiceProvider.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
use App\Models\Room;
|
||||||
|
use App\Observers\RoomObserver;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Room::observe(RoomObserver::class);
|
||||||
|
}
|
||||||
|
}
|
28
app/Providers/VoltServiceProvider.php
Normal file
28
app/Providers/VoltServiceProvider.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
class VoltServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Volt::mount([
|
||||||
|
config('livewire.view_path', resource_path('views/livewire')),
|
||||||
|
resource_path('views/pages'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
187
app/Services/ApiClient.php
Normal file
187
app/Services/ApiClient.php
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
|
use Illuminate\Http\Client\RequestException;
|
||||||
|
use Closure;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
|
||||||
|
class ApiClient
|
||||||
|
{
|
||||||
|
protected string $baseUrl;
|
||||||
|
protected string $token;
|
||||||
|
|
||||||
|
protected int $timeout = 10;
|
||||||
|
protected int $connectTimeout = 5;
|
||||||
|
protected int $retryTimes = 0;
|
||||||
|
protected int $retryDelay = 100;
|
||||||
|
|
||||||
|
protected ?Closure $fallbackHandler = null;
|
||||||
|
protected bool $enableLogging = true;
|
||||||
|
|
||||||
|
public function __construct(string $baseUrl = '', string $token = '')
|
||||||
|
{
|
||||||
|
$this->baseUrl = rtrim($baseUrl, '/');
|
||||||
|
$this->token = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBaseUrl(string $url): self
|
||||||
|
{
|
||||||
|
$this->baseUrl = rtrim($url, '/');
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setToken(string $token): self
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTimeout(int $seconds): self
|
||||||
|
{
|
||||||
|
$this->timeout = $seconds;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConnectTimeout(int $seconds): self
|
||||||
|
{
|
||||||
|
$this->connectTimeout = $seconds;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRetry(int $times, int $delayMs = 100): self
|
||||||
|
{
|
||||||
|
$this->retryTimes = $times;
|
||||||
|
$this->retryDelay = $delayMs;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFallbackHandler(?Closure $handler): self
|
||||||
|
{
|
||||||
|
$this->fallbackHandler = $handler;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enableLogging(bool $value = true): self
|
||||||
|
{
|
||||||
|
$this->enableLogging = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function withDefaultHeaders(): PendingRequest
|
||||||
|
{
|
||||||
|
$request = Http::connectTimeout($this->connectTimeout)
|
||||||
|
->timeout($this->timeout)
|
||||||
|
->withHeaders([
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $this->token,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->retryTimes > 0) {
|
||||||
|
$request = $request->retry($this->retryTimes, $this->retryDelay, function ($exception, $request) {
|
||||||
|
if ($this->enableLogging) {
|
||||||
|
Log::warning('API retrying...', [
|
||||||
|
'url' => $request->url(),
|
||||||
|
'exception' => $exception->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requestWithCatch(callable $fn)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $fn();
|
||||||
|
} catch (RequestException $e) {
|
||||||
|
if ($this->enableLogging) {
|
||||||
|
Log::error('API Request Failed', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'url' => $e->request?->url(),
|
||||||
|
'code' => $e->getCode(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->fallbackHandler instanceof \Closure) {
|
||||||
|
return call_user_func($this->fallbackHandler, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能 fallback,直接寫在這裡
|
||||||
|
$status = $e->response?->status();
|
||||||
|
|
||||||
|
if ($status >= 500) {
|
||||||
|
return ApiResponse::error('伺服器錯誤,請稍後再試','SERVER_ERROR',503);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 404) {
|
||||||
|
return ApiResponse::error('遠端 API 路徑不存在','API_NOT_FOUND',404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 401 || $status === 403) {
|
||||||
|
return ApiResponse::unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($e->getCode() === 0) {
|
||||||
|
return ApiResponse::error('無法連線遠端 API(可能超時或 DNS 錯誤)','CONNECTION_ERROR',504);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::error('API 請求失敗: ' . $e->getMessage(), 'HTTP_CLIENT_ERROR',$status ?? 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $endpoint, array $query = [])
|
||||||
|
{
|
||||||
|
return $this->requestWithCatch(fn () =>
|
||||||
|
$this->withDefaultHeaders()->get("{$this->baseUrl}{$endpoint}", $query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function post(string $endpoint, array $data = [])
|
||||||
|
{
|
||||||
|
return $this->requestWithCatch(fn () =>
|
||||||
|
$this->withDefaultHeaders()->post("{$this->baseUrl}{$endpoint}", $data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $endpoint, array $data = [])
|
||||||
|
{
|
||||||
|
return $this->requestWithCatch(fn () =>
|
||||||
|
$this->withDefaultHeaders()->put("{$this->baseUrl}{$endpoint}", $data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function patch(string $endpoint, array $data = [])
|
||||||
|
{
|
||||||
|
return $this->requestWithCatch(fn () =>
|
||||||
|
$this->withDefaultHeaders()->patch("{$this->baseUrl}{$endpoint}", $data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $endpoint, array $data = [])
|
||||||
|
{
|
||||||
|
return $this->requestWithCatch(fn () =>
|
||||||
|
$this->withDefaultHeaders()->delete("{$this->baseUrl}{$endpoint}", $data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(string $endpoint, array $files = [], array $data = [])
|
||||||
|
{
|
||||||
|
$this->setTimeout(300);
|
||||||
|
$this->setConnectTimeout(10);
|
||||||
|
return $this->requestWithCatch(function () use ($endpoint, $files, $data) {
|
||||||
|
$request = $this->withDefaultHeaders();
|
||||||
|
|
||||||
|
foreach ($files as $key => $filePath) {
|
||||||
|
$filename = basename($filePath);
|
||||||
|
$request = $request->attach($key, fopen($filePath, 'r'), $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->withoutVerifying()->post("{$this->baseUrl}{$endpoint}", $data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
52
app/Services/MachineStatusForwarder.php
Normal file
52
app/Services/MachineStatusForwarder.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Http\Client\Response;
|
||||||
|
use App\Services\ApiClient;
|
||||||
|
|
||||||
|
class MachineStatusForwarder
|
||||||
|
{
|
||||||
|
protected string $externalUrl;
|
||||||
|
protected string $endpoint;
|
||||||
|
protected array $validated;
|
||||||
|
protected ?User $user = null;
|
||||||
|
|
||||||
|
public function __construct(string $externalUrl, string $endpoint, array $validated)
|
||||||
|
{
|
||||||
|
$this->externalUrl = $externalUrl;
|
||||||
|
$this->endpoint = $endpoint;
|
||||||
|
$this->validated = $validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forward(): ?Response
|
||||||
|
{
|
||||||
|
$response = null;
|
||||||
|
$parsed = parse_url($this->externalUrl);
|
||||||
|
$hostParts = explode('.', $parsed['host']);
|
||||||
|
|
||||||
|
$mainDomain = implode('.', array_slice($hostParts, 1));
|
||||||
|
|
||||||
|
$mainDomainUrl = "{$parsed['scheme']}://{$mainDomain}";
|
||||||
|
|
||||||
|
$this->user = User::find(2); // 或用 dependency injection 把 User 帶進來
|
||||||
|
|
||||||
|
if ($this->user && $this->user->api_plain_token) {
|
||||||
|
$client = new ApiClient($mainDomainUrl, $this->user->api_plain_token);
|
||||||
|
$response = $client->post($this->endpoint, $this->validated);
|
||||||
|
|
||||||
|
/* Log::info('✅ Machine status forwarded', [
|
||||||
|
'endpoint' => $this->endpoint,
|
||||||
|
'request' => $this->validated,
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => $response->json(),
|
||||||
|
]); */
|
||||||
|
} else {
|
||||||
|
Log::warning("🔒 User with ID 2 not found or missing token");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
107
app/Services/SqliteExportService.php
Normal file
107
app/Services/SqliteExportService.php
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
|
||||||
|
class SqliteExportService
|
||||||
|
{
|
||||||
|
protected string $connection;
|
||||||
|
|
||||||
|
public function __construct(string $connection)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯出單一模型資料到 SQLite 表。
|
||||||
|
*
|
||||||
|
* @param class-string<Model> $modelClass
|
||||||
|
* @param string $tableName
|
||||||
|
* @param Closure(Blueprint): void $tableSchema
|
||||||
|
* @param Closure(Model): array $transformer
|
||||||
|
* @param int $chunkSize
|
||||||
|
*/
|
||||||
|
public function exportTableFromModel(
|
||||||
|
string $modelClass,
|
||||||
|
string $tableName,
|
||||||
|
Closure $tableSchema,
|
||||||
|
Closure $transformer,
|
||||||
|
int $chunkSize = 1000
|
||||||
|
): void {
|
||||||
|
$this->dropAndCreateTable($tableName, $tableSchema);
|
||||||
|
|
||||||
|
$modelInstance = new $modelClass;
|
||||||
|
|
||||||
|
$modelInstance->newQuery()->orderBy('id')->chunk($chunkSize, function (Collection $chunk) use ($tableName, $transformer) {
|
||||||
|
$rows = $chunk->map($transformer)->toArray();
|
||||||
|
$this->insertData($tableName, $rows);
|
||||||
|
});
|
||||||
|
$this->purge();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批次匯出多張表
|
||||||
|
*
|
||||||
|
* @param array<string, array{
|
||||||
|
* modelClass?: class-string<Model>,
|
||||||
|
* query?: Closure(): \Illuminate\Support\Collection,
|
||||||
|
* tableSchema: Closure(Blueprint): void,
|
||||||
|
* transformer: Closure(Model): array,
|
||||||
|
* chunkSize?: int
|
||||||
|
* }> $tables
|
||||||
|
*/
|
||||||
|
public function exportMultiple(array $tables): void
|
||||||
|
{
|
||||||
|
foreach ($tables as $tableName => $config) {
|
||||||
|
$this->dropAndCreateTable($tableName, $config['tableSchema']);
|
||||||
|
$transformer = $config['transformer'] ?? fn($row) => (array)$row;
|
||||||
|
|
||||||
|
if (isset($config['modelClass'])) {
|
||||||
|
$modelClass = $config['modelClass'];
|
||||||
|
$chunkSize = $config['chunkSize'] ?? 1000;
|
||||||
|
$modelInstance = new $modelClass;
|
||||||
|
|
||||||
|
$modelInstance->newQuery()->chunk($chunkSize, function (Collection $chunk) use ($tableName, $transformer) {
|
||||||
|
$rows = $chunk->map($transformer)->toArray();
|
||||||
|
$this->insertData($tableName, $rows);
|
||||||
|
});
|
||||||
|
} elseif (isset($config['query']) && is_callable($config['query'])) {
|
||||||
|
$rows = call_user_func($config['query']);
|
||||||
|
|
||||||
|
if ($rows instanceof \Illuminate\Database\Query\Builder || $rows instanceof \Illuminate\Database\Eloquent\Builder) {
|
||||||
|
$rows = $rows->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $rows->map($transformer)->toArray();
|
||||||
|
$this->insertData($tableName, $data);
|
||||||
|
} else {
|
||||||
|
throw new \InvalidArgumentException("Each table config must define either 'modelClass' or 'query'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->purge();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function dropAndCreateTable(string $table, Closure $schema): void
|
||||||
|
{
|
||||||
|
Schema::connection($this->connection)->dropIfExists($table);
|
||||||
|
Schema::connection($this->connection)->create($table, $schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function insertData(string $table, array $rows): void
|
||||||
|
{
|
||||||
|
if (empty($rows)) return;
|
||||||
|
DB::connection($this->connection)->table($table)->insert($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function purge(): void
|
||||||
|
{
|
||||||
|
DB::purge($this->connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
app/Services/TcpSocketClient.php
Normal file
43
app/Services/TcpSocketClient.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
class TcpSocketClient
|
||||||
|
{
|
||||||
|
protected $ip;
|
||||||
|
protected $port;
|
||||||
|
protected $timeout;
|
||||||
|
|
||||||
|
public function __construct(string $ip, int $port, int $timeout = 5)
|
||||||
|
{
|
||||||
|
$this->ip = $ip;
|
||||||
|
$this->port = $port;
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(string $data): string
|
||||||
|
{
|
||||||
|
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||||
|
if ($socket === false) {
|
||||||
|
throw new \Exception("Socket create failed: " . socket_strerror(socket_last_error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = socket_connect($socket, $this->ip, $this->port);
|
||||||
|
if ($result === false) {
|
||||||
|
throw new \Exception("Socket connect failed: " . socket_strerror(socket_last_error($socket)));
|
||||||
|
}
|
||||||
|
|
||||||
|
socket_write($socket, $data, strlen($data));
|
||||||
|
|
||||||
|
$response = '';
|
||||||
|
while ($out = socket_read($socket, 2048)) {
|
||||||
|
$response .= $out;
|
||||||
|
// 根據協議判斷是否結束接收,可以自行調整
|
||||||
|
if (strpos($response, "\n") !== false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket_close($socket);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
23
app/Traits/LogsModelActivity.php
Normal file
23
app/Traits/LogsModelActivity.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Spatie\Activitylog\LogOptions;
|
||||||
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
|
||||||
|
trait LogsModelActivity
|
||||||
|
{
|
||||||
|
use LogsActivity;
|
||||||
|
|
||||||
|
public function getActivitylogOptions(): LogOptions
|
||||||
|
{
|
||||||
|
return LogOptions::defaults()
|
||||||
|
->useLogName(strtolower(class_basename(static::class)))
|
||||||
|
->logOnly($this->getFillable())
|
||||||
|
->logOnlyDirty()
|
||||||
|
->dontSubmitEmptyLogs()
|
||||||
|
->setDescriptionForEvent(function (string $eventName) {
|
||||||
|
return class_basename(static::class) . " 已 {$eventName}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
17
app/View/Components/AppLayout.php
Normal file
17
app/View/Components/AppLayout.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AppLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.app');
|
||||||
|
}
|
||||||
|
}
|
17
app/View/Components/GuestLayout.php
Normal file
17
app/View/Components/GuestLayout.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class GuestLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.guest');
|
||||||
|
}
|
||||||
|
}
|
27
app/View/Components/SelectCategory.php
Normal file
27
app/View/Components/SelectCategory.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class SelectCategory extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new component instance.
|
||||||
|
*/
|
||||||
|
public function __construct(public Collection $options, public int $modelId,public string $fieldName, public string $selected)
|
||||||
|
{
|
||||||
|
//dd($options,$modelId,$fieldName,$selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represent the component.
|
||||||
|
*/
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
return view('components.select-category');
|
||||||
|
}
|
||||||
|
}
|
26
app/View/Components/Table.php
Normal file
26
app/View/Components/Table.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class Table extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new component instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represent the component.
|
||||||
|
*/
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
return view('components.table');
|
||||||
|
}
|
||||||
|
}
|
18
artisan
Executable file
18
artisan
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
29
bootstrap/app.php
Normal file
29
bootstrap/app.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->alias([
|
||||||
|
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
|
||||||
|
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
|
||||||
|
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, $request) {
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
return \App\Http\Responses\ApiResponse::unauthorized();
|
||||||
|
}
|
||||||
|
// 其他非 JSON 請求的處理方式(可選)
|
||||||
|
return redirect()->guest(route('login'));
|
||||||
|
});
|
||||||
|
})->create();
|
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
6
bootstrap/providers.php
Normal file
6
bootstrap/providers.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\VoltServiceProvider::class,
|
||||||
|
];
|
90
composer.json
Normal file
90
composer.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": ["laravel", "framework"],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"darkaonline/l5-swagger": "^9.0",
|
||||||
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"league/flysystem-ftp": "^3.29",
|
||||||
|
"livewire/livewire": "^3.4",
|
||||||
|
"livewire/volt": "^1.7.0",
|
||||||
|
"maatwebsite/excel": "^3.1",
|
||||||
|
"openspout/openspout": "^4.0",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/breeze": "^2.3",
|
||||||
|
"laravel/pail": "^1.2.2",
|
||||||
|
"laravel/pint": "^1.13",
|
||||||
|
"laravel/sail": "^1.41",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^11.5.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@php artisan test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
10102
composer.lock
generated
Normal file
10102
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
config/activitylog.php
Normal file
52
config/activitylog.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If set to false, no activities will be saved to the database.
|
||||||
|
*/
|
||||||
|
'enabled' => 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'),
|
||||||
|
];
|
126
config/app.php
Normal file
126
config/app.php
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
115
config/auth.php
Normal file
115
config/auth.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the amount of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
108
config/cache.php
Normal file
108
config/cache.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||||
|
|
||||||
|
];
|
174
config/database.php
Normal file
174
config/database.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => 'prefer',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
380
config/excel.php
Normal file
380
config/excel.php
Normal 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,
|
||||||
|
],
|
||||||
|
];
|
92
config/filesystems.php
Normal file
92
config/filesystems.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'ftp_test' => [
|
||||||
|
'driver' => 'ftp',
|
||||||
|
'host' => env('FTP_HOST', '192.168.11.16'),
|
||||||
|
'username' => env('FTP_USERNAME', 'vod'),
|
||||||
|
'password' => env('FTP_PASSWORD', 'vod'),
|
||||||
|
'port' => 21,
|
||||||
|
'root' => '/', // 或指定資料夾
|
||||||
|
'passive' => true,
|
||||||
|
'ssl' => false,
|
||||||
|
'timeout' => 30,
|
||||||
|
],
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => env('APP_URL').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
318
config/l5-swagger.php
Normal file
318
config/l5-swagger.php
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'default' => 'default',
|
||||||
|
'documentations' => [
|
||||||
|
'default' => [
|
||||||
|
'api' => [
|
||||||
|
'title' => 'L5 Swagger UI',
|
||||||
|
],
|
||||||
|
|
||||||
|
'routes' => [
|
||||||
|
/*
|
||||||
|
* Route for accessing api documentation interface
|
||||||
|
*/
|
||||||
|
'api' => 'api/documentation',
|
||||||
|
],
|
||||||
|
'paths' => [
|
||||||
|
/*
|
||||||
|
* Edit to include full URL in ui for assets
|
||||||
|
*/
|
||||||
|
'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Edit to set path where swagger ui assets should be stored
|
||||||
|
*/
|
||||||
|
'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* File name of the generated json documentation file
|
||||||
|
*/
|
||||||
|
'docs_json' => 'api-docs.json',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* File name of the generated YAML documentation file
|
||||||
|
*/
|
||||||
|
'docs_yaml' => 'api-docs.yaml',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set this to `json` or `yaml` to determine which documentation file to use in UI
|
||||||
|
*/
|
||||||
|
'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Absolute paths to directory containing the swagger annotations are stored.
|
||||||
|
*/
|
||||||
|
'annotations' => [
|
||||||
|
base_path('app'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'defaults' => [
|
||||||
|
'routes' => [
|
||||||
|
/*
|
||||||
|
* Route for accessing parsed swagger annotations.
|
||||||
|
*/
|
||||||
|
'docs' => 'docs',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Route for Oauth2 authentication callback.
|
||||||
|
*/
|
||||||
|
'oauth2_callback' => 'api/oauth2-callback',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Middleware allows to prevent unexpected access to API documentation
|
||||||
|
*/
|
||||||
|
'middleware' => [
|
||||||
|
'api' => [],
|
||||||
|
'asset' => [],
|
||||||
|
'docs' => [],
|
||||||
|
'oauth2_callback' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Route Group options
|
||||||
|
*/
|
||||||
|
'group_options' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'paths' => [
|
||||||
|
/*
|
||||||
|
* Absolute path to location where parsed annotations will be stored
|
||||||
|
*/
|
||||||
|
'docs' => storage_path('api-docs'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Absolute path to directory where to export views
|
||||||
|
*/
|
||||||
|
'views' => base_path('resources/views/vendor/l5-swagger'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Edit to set the api's base path
|
||||||
|
*/
|
||||||
|
'base' => env('L5_SWAGGER_BASE_PATH', null),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Absolute path to directories that should be excluded from scanning
|
||||||
|
* @deprecated Please use `scanOptions.exclude`
|
||||||
|
* `scanOptions.exclude` overwrites this
|
||||||
|
*/
|
||||||
|
'excludes' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'scanOptions' => [
|
||||||
|
/**
|
||||||
|
* Configuration for default processors. Allows to pass processors configuration to swagger-php.
|
||||||
|
*
|
||||||
|
* @link https://zircote.github.io/swagger-php/reference/processors.html
|
||||||
|
*/
|
||||||
|
'default_processors_configuration' => [
|
||||||
|
/** Example */
|
||||||
|
/**
|
||||||
|
* 'operationId.hash' => true,
|
||||||
|
* 'pathFilter' => [
|
||||||
|
* 'tags' => [
|
||||||
|
* '/pets/',
|
||||||
|
* '/store/',
|
||||||
|
* ],
|
||||||
|
* ],.
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* analyser: defaults to \OpenApi\StaticAnalyser .
|
||||||
|
*
|
||||||
|
* @see \OpenApi\scan
|
||||||
|
*/
|
||||||
|
'analyser' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* analysis: defaults to a new \OpenApi\Analysis .
|
||||||
|
*
|
||||||
|
* @see \OpenApi\scan
|
||||||
|
*/
|
||||||
|
'analysis' => null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom query path processors classes.
|
||||||
|
*
|
||||||
|
* @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter
|
||||||
|
* @see \OpenApi\scan
|
||||||
|
*/
|
||||||
|
'processors' => [
|
||||||
|
// new \App\SwaggerProcessors\SchemaQueryParameter(),
|
||||||
|
],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pattern: string $pattern File pattern(s) to scan (default: *.php) .
|
||||||
|
*
|
||||||
|
* @see \OpenApi\scan
|
||||||
|
*/
|
||||||
|
'pattern' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Absolute path to directories that should be excluded from scanning
|
||||||
|
* @note This option overwrites `paths.excludes`
|
||||||
|
* @see \OpenApi\scan
|
||||||
|
*/
|
||||||
|
'exclude' => [],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0.
|
||||||
|
* By default the spec will be in version 3.0.0
|
||||||
|
*/
|
||||||
|
'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', \L5Swagger\Generator::OPEN_API_DEFAULT_SPEC_VERSION),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* API security definitions. Will be generated into documentation file.
|
||||||
|
*/
|
||||||
|
'securityDefinitions' => [
|
||||||
|
'securitySchemes' => [
|
||||||
|
/*
|
||||||
|
* Examples of Security schemes
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
'api_key_security_example' => [ // Unique name of security
|
||||||
|
'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
|
||||||
|
'description' => 'A short description for security scheme',
|
||||||
|
'name' => 'api_key', // The name of the header or query parameter to be used.
|
||||||
|
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
|
||||||
|
],
|
||||||
|
'oauth2_security_example' => [ // Unique name of security
|
||||||
|
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
|
||||||
|
'description' => 'A short description for oauth2 security scheme.',
|
||||||
|
'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode".
|
||||||
|
'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode)
|
||||||
|
//'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode)
|
||||||
|
'scopes' => [
|
||||||
|
'read:projects' => 'read your projects',
|
||||||
|
'write:projects' => 'modify projects in your account',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Open API 3.0 support
|
||||||
|
'passport' => [ // Unique name of security
|
||||||
|
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
|
||||||
|
'description' => 'Laravel passport oauth2 security.',
|
||||||
|
'in' => 'header',
|
||||||
|
'scheme' => 'https',
|
||||||
|
'flows' => [
|
||||||
|
"password" => [
|
||||||
|
"authorizationUrl" => config('app.url') . '/oauth/authorize',
|
||||||
|
"tokenUrl" => config('app.url') . '/oauth/token',
|
||||||
|
"refreshUrl" => config('app.url') . '/token/refresh',
|
||||||
|
"scopes" => []
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'sanctum' => [ // Unique name of security
|
||||||
|
'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2".
|
||||||
|
'description' => 'Enter token in format (Bearer <token>)',
|
||||||
|
'name' => 'Authorization', // The name of the header or query parameter to be used.
|
||||||
|
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
'security' => [
|
||||||
|
/*
|
||||||
|
* Examples of Securities
|
||||||
|
*/
|
||||||
|
[
|
||||||
|
/*
|
||||||
|
'oauth2_security_example' => [
|
||||||
|
'read',
|
||||||
|
'write'
|
||||||
|
],
|
||||||
|
|
||||||
|
'passport' => []
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set this to `true` in development mode so that docs would be regenerated on each request
|
||||||
|
* Set this to `false` to disable swagger generation on production
|
||||||
|
*/
|
||||||
|
'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set this to `true` to generate a copy of documentation in yaml format
|
||||||
|
*/
|
||||||
|
'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Edit to trust the proxy's ip address - needed for AWS Load Balancer
|
||||||
|
* string[]
|
||||||
|
*/
|
||||||
|
'proxy' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle.
|
||||||
|
* See more at: https://github.com/swagger-api/swagger-ui#configs-plugin
|
||||||
|
*/
|
||||||
|
'additional_config_url' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically),
|
||||||
|
* 'method' (sort by HTTP method).
|
||||||
|
* Default is the order returned by the server unchanged.
|
||||||
|
*/
|
||||||
|
'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pass the validatorUrl parameter to SwaggerUi init on the JS side.
|
||||||
|
* A null value here disables validation.
|
||||||
|
*/
|
||||||
|
'validator_url' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Swagger UI configuration parameters
|
||||||
|
*/
|
||||||
|
'ui' => [
|
||||||
|
'display' => [
|
||||||
|
'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false),
|
||||||
|
/*
|
||||||
|
* Controls the default expansion setting for the operations and tags. It can be :
|
||||||
|
* 'list' (expands only the tags),
|
||||||
|
* 'full' (expands the tags and operations),
|
||||||
|
* 'none' (expands nothing).
|
||||||
|
*/
|
||||||
|
'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set, enables filtering. The top bar will show an edit box that
|
||||||
|
* you can use to filter the tagged operations that are shown. Can be
|
||||||
|
* Boolean to enable or disable, or a string, in which case filtering
|
||||||
|
* will be enabled using that string as the filter expression. Filtering
|
||||||
|
* is case-sensitive matching the filter expression anywhere inside
|
||||||
|
* the tag.
|
||||||
|
*/
|
||||||
|
'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false
|
||||||
|
],
|
||||||
|
|
||||||
|
'authorization' => [
|
||||||
|
/*
|
||||||
|
* If set to true, it persists authorization data, and it would not be lost on browser close/refresh
|
||||||
|
*/
|
||||||
|
'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false),
|
||||||
|
|
||||||
|
'oauth2' => [
|
||||||
|
/*
|
||||||
|
* If set to true, adds PKCE to AuthorizationCodeGrant flow
|
||||||
|
*/
|
||||||
|
'use_pkce_with_authorization_code_grant' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
* Constants which can be used in annotations
|
||||||
|
*/
|
||||||
|
'constants' => [
|
||||||
|
'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
166
config/livewire-powergrid.php
Normal file
166
config/livewire-powergrid.php
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Js;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Theme
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| PowerGrid supports Tailwind and Bootstrap 5 themes.
|
||||||
|
| Configure here the theme of your choice.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'theme' => \PowerComponents\LivewirePowerGrid\Themes\Tailwind::class,
|
||||||
|
// 'theme' => \PowerComponents\LivewirePowerGrid\Themes\DaisyUI::class,
|
||||||
|
// 'theme' => \PowerComponents\LivewirePowerGrid\Themes\Bootstrap5::class,
|
||||||
|
|
||||||
|
'cache_ttl' => null,
|
||||||
|
|
||||||
|
'icon_resources' => [
|
||||||
|
'paths' => [
|
||||||
|
// 'default' => 'resources/views/components/icons',
|
||||||
|
// 'outline' => 'vendor/wireui/wireui/resources/views/components/icons/outline',
|
||||||
|
// 'solid' => 'vendor/wireui/wireui/resources/views/components/icons/solid',
|
||||||
|
'outline' => 'vendor/wireui/heroicons/src/views/components/outline',
|
||||||
|
'solid' => 'vendor/wireui/heroicons/src/views/components/solid',
|
||||||
|
],
|
||||||
|
|
||||||
|
'allowed' => [
|
||||||
|
// 'pencil',
|
||||||
|
],
|
||||||
|
|
||||||
|
'attributes' => ['class' => 'w-5 text-red-600'],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Plugins
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Plugins used: flatpickr.js to datepicker.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'plugins' => [
|
||||||
|
/*
|
||||||
|
* https://flatpickr.js.org
|
||||||
|
*/
|
||||||
|
'flatpickr' => [
|
||||||
|
'locales' => [
|
||||||
|
'zh_tw' => [
|
||||||
|
'locale' => 'zh_tw',
|
||||||
|
'dateFormat' => 'Y-m-d H:i',
|
||||||
|
'enableTime' => true,
|
||||||
|
'time_24hr' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'select' => [
|
||||||
|
'default' => 'tom',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TomSelect Options
|
||||||
|
* https://tom-select.js.org
|
||||||
|
*/
|
||||||
|
'tom' => [
|
||||||
|
'plugins' => [
|
||||||
|
'clear_button' => [
|
||||||
|
'title' => 'Remove all selected options',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Slim Select options
|
||||||
|
* https://slimselectjs.com/
|
||||||
|
*/
|
||||||
|
'slim' => [
|
||||||
|
'settings' => [
|
||||||
|
'alwaysOpen' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| PowerGrid supports inline and outside filters.
|
||||||
|
| 'inline': Filters data inside the table.
|
||||||
|
| 'outside': Filters data outside the table.
|
||||||
|
| 'null'
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'filter' => 'inline',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filters Attributes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
| You can add custom attributes to the filters.
|
||||||
|
| The key is the filter type and the value is a callback function.
|
||||||
|
| like: input_text, select, datetime, etc.
|
||||||
|
| The callback function receives the field and title as parameters.
|
||||||
|
| The callback function must return an array with the attributes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'filter_attributes' => [
|
||||||
|
'input_text' => \PowerComponents\LivewirePowerGrid\FilterAttributes\InputText::class,
|
||||||
|
'boolean' => \PowerComponents\LivewirePowerGrid\FilterAttributes\Boolean::class,
|
||||||
|
'number' => \PowerComponents\LivewirePowerGrid\FilterAttributes\Number::class,
|
||||||
|
'select' => \PowerComponents\LivewirePowerGrid\FilterAttributes\Select::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Persisting
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| PowerGrid supports persisting of the filters, columns and sorting.
|
||||||
|
| 'session': persist in the session.
|
||||||
|
| 'cache': persist with cache.
|
||||||
|
| 'cookies': persist with cookies (default).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'persist_driver' => 'cookies',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Exportable class
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'exportable' => [
|
||||||
|
'default' => 'openspout_v4',
|
||||||
|
'openspout_v4' => [
|
||||||
|
'xlsx' => \PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v4\ExportToXLS::class,
|
||||||
|
'csv' => \PowerComponents\LivewirePowerGrid\Components\Exports\OpenSpout\v4\ExportToCsv::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Auto-Discover Models
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| PowerGrid will search for Models in the directories listed below.
|
||||||
|
| These Models be listed as options when you run the
|
||||||
|
| "artisan powergrid:create" command.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'auto_discover_models_paths' => [
|
||||||
|
app_path('Models'),
|
||||||
|
],
|
||||||
|
];
|
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
202
config/permission.php
Normal file
202
config/permission.php
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||||
|
* is often just the "Permission" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Permission model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Permission` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permission' => Spatie\Permission\Models\Permission::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||||
|
* is often just the "Role" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Role model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => Spatie\Permission\Models\Role::class,
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'roles' => 'roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your permissions. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models roles. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related pivots other than defaults
|
||||||
|
*/
|
||||||
|
'role_pivot_key' => null, // default 'role_id',
|
||||||
|
'permission_pivot_key' => null, // default 'permission_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related model primary key other than
|
||||||
|
* `model_id`.
|
||||||
|
*
|
||||||
|
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||||
|
* that case, name this `model_uuid`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to use the teams feature and your related model's
|
||||||
|
* foreign key is other than `team_id`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'team_foreign_key' => 'team_id',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the method for checking permissions will be registered on the gate.
|
||||||
|
* Set this to false if you want to implement custom logic for checking permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||||
|
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||||
|
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||||
|
*/
|
||||||
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Events will fire when a role or permission is assigned/unassigned:
|
||||||
|
* \Spatie\Permission\Events\RoleAttached
|
||||||
|
* \Spatie\Permission\Events\RoleDetached
|
||||||
|
* \Spatie\Permission\Events\PermissionAttached
|
||||||
|
* \Spatie\Permission\Events\PermissionDetached
|
||||||
|
*
|
||||||
|
* To enable, set to true, and then create listeners to watch these events.
|
||||||
|
*/
|
||||||
|
'events_enabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Teams Feature.
|
||||||
|
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||||
|
* If you want the migrations to register the 'team_foreign_key', you must
|
||||||
|
* set this to true before doing the migration.
|
||||||
|
* If you already did the migration then you must make a new migration to also
|
||||||
|
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||||
|
* (view the latest version of this package's migration file)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'teams' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use to resolve the permissions team id
|
||||||
|
*/
|
||||||
|
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Passport Client Credentials Grant
|
||||||
|
* When set to true the package will use Passports Client to check permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required permission names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required role names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default wildcard permission lookups are disabled.
|
||||||
|
* See documentation to understand supported syntax.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use for interpreting wildcard permissions.
|
||||||
|
* If you need to modify delimiters, override the class and specify its name here.
|
||||||
|
*/
|
||||||
|
// 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||||
|
|
||||||
|
/* Cache-specific settings */
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all permissions are cached for 24 hours to speed up performance.
|
||||||
|
* When permissions or roles are updated the cache is flushed automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The cache key used to store all permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may optionally indicate a specific cache driver to use for permission and
|
||||||
|
* role caching using any of the `store` drivers listed in the cache.php config
|
||||||
|
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => 'default',
|
||||||
|
],
|
||||||
|
];
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user