Andrew 10 hours ago
parent
commit
8318b260b3

+ 87
- 0
app/Filament/AlbumCategoryResource.php View File

@@ -0,0 +1,87 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\AlbumCategoryResource\Pages;
6
+use App\Filament\Resources\AlbumCategoryResource\RelationManagers;
7
+use App\Models\AlbumCategory;
8
+use Filament\Forms;
9
+use Filament\Forms\Components\Actions\Action;
10
+use Filament\Forms\Components\Section;
11
+use Filament\Forms\Components\TextInput;
12
+use Filament\Forms\Form;
13
+use Filament\Resources\Resource;
14
+use Filament\Tables;
15
+use Filament\Tables\Columns\TextColumn;
16
+use Filament\Tables\Table;
17
+use Illuminate\Database\Eloquent\Builder;
18
+use Illuminate\Database\Eloquent\SoftDeletingScope;
19
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
20
+use App\Service\DeepLService;
21
+
22
+class AlbumCategoryResource extends Resource
23
+{
24
+    protected static ?string $model = AlbumCategory::class;
25
+    protected static ?string $modelLabel = "影音類別管理";
26
+    protected static ?string $navigationIcon = 'heroicon-o-rectangle-group';
27
+    protected static ?string $navigationGroup = '上稿內容管理';
28
+    protected static ?string $navigationLabel = "影音類別管理";
29
+
30
+    public static function form(Form $form): Form
31
+    {
32
+        return $form
33
+            ->schema([
34
+                //
35
+                Section::make("")->schema([
36
+                    Translate::make()->schema(fn (string $locale) => [
37
+                        TextInput::make('name')->required($locale == 'zh_TW')->maxLength(40)->label("分類名稱")
38
+                    ])
39
+                    ->locales(["zh_TW", "en", "jp"])
40
+                    ->actions([
41
+                        app(DeepLService::class)->createTranslationAction("Main", ["name"])
42
+                    ])->columnSpan(5),
43
+                    TextInput::make('order')->label("排序")->integer()->default(0)->columnSpan(1)
44
+                ])->columns(4)
45
+            ]);
46
+    }
47
+
48
+    public static function table(Table $table): Table
49
+    {
50
+        return $table
51
+            ->columns([
52
+                TextColumn::make('name')->label("分類名稱"),
53
+                TextColumn::make(name: 'created_at')->label("建立時間")->date(),
54
+                TextColumn::make('updated_at')->label("更新時間")->date()
55
+            ])
56
+            ->filters([
57
+                //
58
+            ])
59
+            ->actions([
60
+                // Tables\Actions\ViewAction::make(),
61
+                Tables\Actions\EditAction::make(),
62
+                Tables\Actions\DeleteAction::make()
63
+            ])
64
+            ->bulkActions([
65
+                Tables\Actions\BulkActionGroup::make([
66
+                    Tables\Actions\DeleteBulkAction::make(),
67
+                ]),
68
+            ])
69
+            ->defaultSort('order', 'desc');
70
+    }
71
+
72
+    public static function getRelations(): array
73
+    {
74
+        return [
75
+            //
76
+        ];
77
+    }
78
+
79
+    public static function getPages(): array
80
+    {
81
+        return [
82
+            'index' => Pages\ListAlbumCategories::route('/'),
83
+            'create' => Pages\CreateAlbumCategory::route('/create'),
84
+            'edit' => Pages\EditAlbumCategory::route('/{record}/edit'),
85
+        ];
86
+    }
87
+}

+ 17
- 0
app/Filament/AlbumCategoryResource/Pages/CreateAlbumCategory.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\CreateRecord;
8
+
9
+class CreateAlbumCategory extends CreateRecord
10
+{
11
+    protected static string $resource = AlbumCategoryResource::class;
12
+    protected static bool $canCreateAnother = false;
13
+    protected function getRedirectUrl(): string
14
+    {
15
+        return $this->getResource()::getUrl('index');
16
+    }
17
+}

+ 23
- 0
app/Filament/AlbumCategoryResource/Pages/EditAlbumCategory.php View File

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditAlbumCategory extends EditRecord
10
+{
11
+    protected static string $resource = AlbumCategoryResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            // Actions\DeleteAction::make(),
17
+        ];
18
+    }
19
+    protected function getRedirectUrl(): string
20
+    {
21
+        return $this->getResource()::getUrl('index');
22
+    }
23
+}

+ 19
- 0
app/Filament/AlbumCategoryResource/Pages/ListAlbumCategories.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListAlbumCategories extends ListRecords
10
+{
11
+    protected static string $resource = AlbumCategoryResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make()->label("新增"),
17
+        ];
18
+    }
19
+}

+ 12
- 0
app/Filament/AlbumCategoryResource/Pages/ViewAlbumCategory.php View File

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ViewRecord;
8
+
9
+class ViewAlbumCategory extends ViewRecord
10
+{
11
+    protected static string $resource = AlbumCategoryResource::class;
12
+}

+ 202
- 0
app/Filament/AlbumResource.php View File

@@ -0,0 +1,202 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\AlbumResource\Pages;
6
+use App\Filament\Resources\AlbumResource\RelationManagers;
7
+use App\Service\DeepLService;
8
+use App\Models\Album;
9
+use App\Models\AlbumCategory;
10
+use App\Models\NewsCategory;
11
+use Filament\Forms;
12
+use Filament\Forms\Components\Actions\Action;
13
+use Filament\Forms\Components\DatePicker;
14
+use Filament\Forms\Components\DateTimePicker;
15
+use Filament\Forms\Components\FileUpload;
16
+use Filament\Forms\Components\Group;
17
+use Filament\Forms\Components\Radio;
18
+use Filament\Forms\Components\Section;
19
+use Filament\Forms\Components\Select;
20
+use Filament\Forms\Components\Textarea;
21
+use Filament\Forms\Components\TextInput;
22
+use Filament\Forms\Components\Toggle;
23
+use Filament\Forms\Form;
24
+use Filament\Forms\Get;
25
+use Filament\Resources\Resource;
26
+use Filament\Tables;
27
+use Filament\Tables\Columns\ImageColumn;
28
+use Filament\Tables\Columns\TextColumn;
29
+use Filament\Tables\Filters\QueryBuilder;
30
+use Filament\Tables\Filters\QueryBuilder\Constraints\SelectConstraint;
31
+use Filament\Tables\Filters\QueryBuilder\Constraints\TextConstraint;
32
+use Filament\Tables\Filters\SelectFilter;
33
+use Filament\Tables\Table;
34
+use Illuminate\Database\Eloquent\Builder;
35
+use Illuminate\Database\Eloquent\Collection;
36
+use Illuminate\Database\Eloquent\SoftDeletingScope;
37
+use Illuminate\Support\Facades\DB;
38
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
39
+
40
+class AlbumResource extends Resource
41
+{
42
+    protected static ?string $model = Album::class;
43
+    protected static ?string $modelLabel = "影音管理";
44
+    protected static ?string $navigationIcon = 'heroicon-o-photo';
45
+    protected static ?string $navigationGroup = '上稿內容管理';
46
+    protected static ?string $navigationLabel = "影音管理";
47
+
48
+    public static function form(Form $form): Form
49
+    {
50
+        return $form
51
+            ->schema([
52
+                Section::make("新增 影音")->schema([
53
+                    Group::make()->schema([
54
+                        Select::make("album_category_id")
55
+                        ->options(AlbumCategory::orderBy("order")->get()->pluck("name","id"))
56
+                        ->label("影音分類")
57
+                        ->required()
58
+                        ->columnSpan(1)
59
+                        ->native(false)
60
+                        ->Live(),
61
+                    ])->columnSpanFull()->columns(2),
62
+                    Group::make()->schema([
63
+                        DatePicker::make('post_date')
64
+                        ->label("發布日期")
65
+                        ->closeOnDateSelection(),
66
+                    ])->columnSpanFull()->columns(2),
67
+                    FileUpload::make('news_banner')->label("列表大圖")
68
+                    ->disk("s3")
69
+                    ->directory("album/img")
70
+                    ->helperText('建議寬高限制為:2000*720px,出血寬度720px,主要圖像範圍為:1280*720px,檔案大小限制為1M以下')->maxSize('1024'),
71
+                    FileUpload::make('news_img_pc')->label("列表圖(desktop)")
72
+                    ->disk("s3")
73
+                    ->directory("album/img")
74
+                    ->helperText('建議寬高限制為:1280*720px,檔案大小限制為1M以下')->maxSize('1024'),
75
+                    FileUpload::make('news_img_mobile')->label("列表圖(mobile)")
76
+                    ->disk("s3")
77
+                    ->directory("album/img")
78
+                    ->helperText('建議寬高限制為:600x896px,檔案大小限制為1M以下')->maxSize('1024'),
79
+                    Translate::make()->schema(fn (string $locale) => [
80
+                        TextInput::make('title')
81
+                        ->label("標題")
82
+                        ->columnSpan(1),
83
+                    ])
84
+                    ->locales(["zh_TW", "en", "jp"])
85
+                    ->actions([
86
+                        app(DeepLService::class)->createTranslationAction("Main", ["title"])
87
+                    ])
88
+                    ->id("main")->columnSpanFull()->columns(3),
89
+
90
+                    Section::make("")->schema([
91
+                        Radio::make("upload_type")->label("")->options([
92
+                            1 => "網址",
93
+                            2 => "檔案"
94
+                        ])->columnSpanFull()->default(1)->Live(),
95
+                        Group::make()->schema([
96
+                            TextInput::make('link_video')->label("網址")->nullable(),
97
+                        ])->visible(fn (Get $get):bool => $get("upload_type") == 1)->columnSpanFull(),
98
+                        Group::make()->schema([
99
+                            FileUpload::make('link_upload')->label("")->disk("s3")->directory("album/video")
100
+                            ->helperText('建議影片寬高限制為:1920*1080px,出血寬度720px,大小限制為:100M以下')
101
+                            ->maxSize(102400)->nullable(),
102
+                        ])->visible(fn (Get $get):bool => $get("upload_type") == 2)->columnSpanFull(),
103
+                    ])->columnSpanFull(),
104
+                    Toggle::make("on_top")->inline()->label("置頂輪播")->columnSpanFull(),
105
+                    Toggle::make("homepage_top")->inline()->label("在首頁輪播")->columnSpanFull(),
106
+                    TextInput::make('order')->label("排序")->integer()->default(0),
107
+                ])->columns(3),
108
+            ]);
109
+    }
110
+
111
+    public static function table(Table $table): Table
112
+    {
113
+        return $table
114
+            ->columns([
115
+                //
116
+                TextColumn::make("albumCategory.name")->label("分類")->alignCenter(),
117
+                TextColumn::make("title")->label("標題")->alignCenter(),
118
+                TextColumn::make("post_date")->date()->alignCenter(),
119
+                ImageColumn::make("news_img_pc")->disk('s3')->alignCenter(),
120
+                TextColumn::make("list_audit_state")->label("狀態")->badge()
121
+                ->color(fn (string $state): string => match ($state) {
122
+                    '暫存' => 'warning',
123
+                    '已發佈' => 'success',
124
+                }),
125
+                TextColumn::make("created_at")->label("建立時間")->dateTime()->alignCenter(),
126
+                TextColumn::make("updated_at")->label("更新時間")->dateTime()->alignCenter(),
127
+            ])
128
+            ->filters([
129
+                SelectFilter::make('post_date')->label("年份")
130
+                ->options(Album::select(DB::raw("DATE_FORMAT(post_date, '%Y') as year"))->whereNotNull("post_date")->distinct()->pluck("year","year")->toArray())
131
+                ->query(
132
+                    fn (array $data, Builder $query): Builder =>
133
+                    $query->when(
134
+                        $data['value'],
135
+                        fn (Builder $query, $value): Builder => $query->where('post_date', 'like', $data['value']. "%")
136
+                    )
137
+                ),
138
+                SelectFilter::make('album_category_id')->label("分類")
139
+                ->relationship('albumCategory', 'name')
140
+                ->getOptionLabelFromRecordUsing(fn($record, $livewire) => $record->getTranslation('name', "zh_TW")),
141
+                SelectFilter::make('visible')->label("狀態")
142
+                ->options([
143
+                    0 => "暫存",
144
+                    1 => "已發佈",
145
+                ])
146
+                ->query(
147
+                    fn (array $data, Builder $query): Builder =>
148
+                    $query->when(
149
+                        $data['value'],
150
+                        fn (Builder $query, $value): Builder => $query->where('visible', $data['value'])
151
+                    )
152
+                ),
153
+            ])
154
+            ->actions([
155
+                Tables\Actions\EditAction::make(),
156
+                Tables\Actions\DeleteAction::make(),
157
+                \Filament\Tables\Actions\Action::make("audit")
158
+                ->label(fn ($record) => match ($record->visible) {
159
+                    0 => '發佈',
160
+                    1 => '下架',
161
+                })
162
+                ->color(fn ($record) => match ($record->visible) {
163
+                    0 => 'warning',
164
+                    1 => 'gray',
165
+                })
166
+                ->icon(fn ($record) => match ($record->visible) {
167
+                    0 => 'heroicon-m-chevron-double-up',
168
+                    1 => 'heroicon-m-chevron-double-down',
169
+                })
170
+                ->action(function ($record) {
171
+                    $record->visible = !$record->visible;
172
+                    $record->save();
173
+                })
174
+                ->outlined()
175
+                ->requiresConfirmation(),
176
+            ])
177
+            ->bulkActions([
178
+                Tables\Actions\BulkActionGroup::make([
179
+                    Tables\Actions\DeleteBulkAction::make(),
180
+                ]),
181
+            ])
182
+            ->defaultSort('order', 'desc')
183
+            ->defaultSort('created_at', 'desc');
184
+    }
185
+
186
+    public static function getRelations(): array
187
+    {
188
+        return [
189
+            //
190
+        ];
191
+    }
192
+
193
+    public static function getPages(): array
194
+    {
195
+        return [
196
+            'index' => Pages\ListAlbums::route('/'),
197
+            'create' => Pages\CreateAlbum::route('/create'),
198
+            'edit' => Pages\EditAlbum::route('/{record}/edit'),
199
+            'view' => Pages\ViewAlbum::route('/{record}/view'),
200
+        ];
201
+    }
202
+}

+ 31
- 0
app/Filament/AlbumResource/Pages/CreateAlbum.php View File

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumResource;
6
+use App\Models\Album;
7
+use Filament\Actions;
8
+use Filament\Resources\Pages\CreateRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+class CreateAlbum extends CreateRecord
12
+{
13
+    protected static string $resource = AlbumResource::class;
14
+    protected static bool $canCreateAnother = false;
15
+
16
+    protected function getRedirectUrl(): string
17
+    {
18
+        return $this->getResource()::getUrl('index');
19
+    }
20
+    protected function handleRecordCreation(array $data): Model
21
+    {
22
+        $data['link'] = "";
23
+        if($data["upload_type"] == 1){
24
+            $data['link'] = $data["link_video"];
25
+        }elseif($data["upload_type"] == 2){
26
+            $data['link'] = $data["link_upload"];
27
+        }
28
+
29
+        return static::getModel()::create($data);
30
+    }
31
+}

+ 40
- 0
app/Filament/AlbumResource/Pages/EditAlbum.php View File

@@ -0,0 +1,40 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumResource;
6
+use App\Models\Album;
7
+use Filament\Actions;
8
+use Filament\Resources\Pages\EditRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+class EditAlbum extends EditRecord
12
+{
13
+    protected static string $resource = AlbumResource::class;
14
+
15
+    protected function getHeaderActions(): array
16
+    {
17
+        return [
18
+            // Actions\DeleteAction::make(),
19
+        ];
20
+    }
21
+    protected function mutateFormDataBeforeFill(array $data): array
22
+    {
23
+        if($data["upload_type"] == 1){
24
+            $data['link_video'] = $data["link"];
25
+        }elseif($data["upload_type"] == 2){
26
+            $data['link_upload'] = $data["link"];
27
+        }
28
+        return $data;
29
+    }
30
+    protected function handleRecordUpdate(Model $record, array $data): Model
31
+    {
32
+        if($data["upload_type"] == 1){
33
+            $data['link'] = $data["link_video"];
34
+        }elseif($data["upload_type"] == 2){
35
+            $data['link'] = $data["link_upload"];
36
+        }
37
+        $record->update($data);
38
+        return $record;
39
+    }
40
+}

+ 19
- 0
app/Filament/AlbumResource/Pages/ListAlbums.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListAlbums extends ListRecords
10
+{
11
+    protected static string $resource = AlbumResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make()->label("新增"),
17
+        ];
18
+    }
19
+}

+ 12
- 0
app/Filament/AlbumResource/Pages/ViewAlbum.php View File

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\AlbumResource\Pages;
4
+
5
+use App\Filament\Resources\AlbumResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ViewRecord;
8
+
9
+class ViewAlbum extends ViewRecord
10
+{
11
+    protected static string $resource = AlbumResource::class;
12
+}

+ 82
- 0
app/Http/Controllers/Api/AlbumController.php View File

@@ -0,0 +1,82 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\Album;
7
+use App\Models\AlbumCategory;
8
+use App\Supports\Response;
9
+use Carbon\Carbon;
10
+use Illuminate\Http\Request;
11
+use Illuminate\Support\Facades\DB;
12
+use Illuminate\Support\Facades\Log;
13
+
14
+/**
15
+ * @group Lottery Prize
16
+ */
17
+class AlbumController extends Controller
18
+{
19
+    public function __construct(
20
+    )
21
+    {
22
+    }
23
+
24
+    public function list(Request $request, $locate = 'tw')
25
+    {
26
+        $categoryId = $request->input("categoryId") ?? "";
27
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
28
+        $result = [];
29
+
30
+        //年份清單
31
+        $yearList = Album::select(DB::raw("DATE_FORMAT(post_date, '%Y') as years"))->distinct()->orderBy("years", "desc")->pluck("years");
32
+        $result["yearList"] = $yearList;
33
+
34
+        //置頂影音
35
+        $topVideos = Album::where("visible", 1)->where("on_top", 1)->orderBy("order")->orderByDesc("post_date")->get();
36
+        foreach($topVideos as $topVideo){
37
+            $result["top"][] = [
38
+                "id" => $topVideo->id,
39
+                "categoryId" => $topVideo->albumCategory->id,
40
+                "category" => $topVideo->albumCategory->getTranslation("name", $locate),
41
+                "postDate" => Carbon::parse($topVideo->post_date)->format("Y.m.d"),
42
+                "title" => $topVideo->getTranslation("title", $locate),
43
+                "imgBanner" => $topVideo->album_img_banner_url,
44
+                "imgPC" => $topVideo->album_img_pc_url,
45
+                "imgMobile" => $topVideo->album_img_mobile_url,
46
+                "type" => $topVideo->album_upload_type,
47
+                "link" => $topVideo->album_video_link,
48
+            ];
49
+        }
50
+
51
+        //分類列表
52
+        $categoryList = AlbumCategory::orderByDesc('order')->orderByDesc('id')->get();
53
+        foreach($categoryList as $listItem){
54
+            $result["categoryList"][] = [
55
+                "id" => $listItem->id,
56
+                "name" => $listItem->getTranslation("name", $locate),
57
+            ];
58
+        }
59
+
60
+        //影音列表
61
+        $albums = Album::where("visible", 1);
62
+        if($categoryId){
63
+            $albums->where("album_category_id", $categoryId);
64
+        }
65
+        $albums = $albums->orderByDesc("order")->orderByDesc("post_date")->get();
66
+        foreach($albums as $item){
67
+            $result["list"][] = [
68
+                "id" => $item->id,
69
+                "categoryId" => $item->albumCategory->id,
70
+                "category" => $item->albumCategory->getTranslation("name", $locate),
71
+                "postDate" => Carbon::parse($item->post_date)->format("Y.m.d"),
72
+                "title" => $item->getTranslation("title", $locate),
73
+                "imgBanner" => $item->album_img_banner_url,
74
+                "imgPC" => $item->album_img_pc_url,
75
+                "imgMobile" => $item->album_img_mobile_url,
76
+                "type" => $item->album_upload_type,
77
+                "link" => $item->album_video_link,
78
+            ];
79
+        }
80
+        return Response::ok($result);
81
+    }
82
+}

+ 105
- 0
app/Models/Album.php View File

@@ -0,0 +1,105 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
6
+use Illuminate\Database\Eloquent\Factories\HasFactory;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Database\Eloquent\SoftDeletes;
9
+use Illuminate\Support\Facades\Storage;
10
+use Spatie\Translatable\HasTranslations;
11
+
12
+class Album extends Model
13
+{
14
+    use HasFactory, SoftDeletes, HasTranslations;
15
+
16
+    protected $casts = [
17
+        'on_top' => 'boolean',
18
+        "list_audit_state" => "string"
19
+    ];
20
+
21
+    protected $guarded = ['id'];
22
+    protected $appends = ["album_img_pc_url", "album_img_pc_url", "album_img_banner_url", "album_img_mobile_url", "album_meta_img", "album_upload_type", "album_video_link"];
23
+
24
+    public $translatable = ['title', 'description', 'meta_title', 'meta_description', 'meta_keyword'];
25
+
26
+    public function albumCategory(){
27
+        return $this->belongsTo(AlbumCategory::class);
28
+    }
29
+    protected function albumImgPcUrl(): Attribute
30
+    {
31
+        return Attribute::make(
32
+            get: fn ($value) => is_null($this->news_img_pc) ? null :Storage::disk('s3')->url($this->news_img_pc),
33
+        );
34
+    }
35
+    protected function albumImgMobileUrl(): Attribute
36
+    {
37
+        return Attribute::make(
38
+            get: fn ($value) => is_null($this->news_img_mobile) ? null :Storage::disk('s3')->url($this->news_img_mobile),
39
+        );
40
+    }
41
+    protected function albumMetaImg(): Attribute
42
+    {
43
+        return Attribute::make(
44
+            get: fn ($value) => is_null($this->meta_img) ? null :Storage::disk('s3')->url($this->meta_img),
45
+        );
46
+    }
47
+    protected function albumImgBannerUrl(): Attribute
48
+    {
49
+        return Attribute::make(
50
+            get: fn ($value) => is_null($this->news_banner) ? null :Storage::disk('s3')->url($this->news_banner),
51
+        );
52
+    }
53
+    protected function albumUploadType(): Attribute
54
+    {
55
+        return Attribute::make(
56
+            get: fn ($value) => $this->attributes["upload_type"] == 1 ? "url" : "upload",
57
+        );
58
+    }
59
+
60
+    protected function listAuditState(): Attribute
61
+    {
62
+        return Attribute::make(
63
+            get: fn ($value) => $this->attributes["visible"] == 1 ? "已發佈" : "暫存",
64
+        );
65
+    }
66
+    protected function albumVideoLink(): Attribute
67
+    {
68
+        return Attribute::make(
69
+            get: fn ($value) => ($this->attributes["upload_type"] == 2) ? Storage::disk('s3')->url($this->attributes["link"]) : $this->attributes["link"],
70
+        );
71
+    }
72
+    public function homepageToTop()
73
+    {
74
+        return $this->morphOne(HomepageToTop::class, 'resource');
75
+    }
76
+    protected static function booted()
77
+    {
78
+        static::created(function ($album) {
79
+            if ($album->homepage_top) {
80
+                $album->createOrUpdatePageSet();
81
+            }
82
+        });
83
+
84
+        static::updated(function ($album) {
85
+            if ($album->isDirty('homepage_top')) {
86
+                if ($album->homepage_top) {
87
+                    $album->createOrUpdatePageSet();
88
+                } else {
89
+                    $album->homepageToTop()->delete();
90
+                }
91
+            }
92
+        });
93
+        static::deleted(function ($album) {
94
+                $album->homepageToTop()->delete();
95
+        });
96
+    }
97
+
98
+    protected function createOrUpdatePageSet()
99
+    {
100
+        $this->homepageToTop()->updateOrCreate(
101
+            [],
102
+            ['resource' => 'video']
103
+        );
104
+    }
105
+}

+ 21
- 0
app/Models/AlbumCategory.php View File

@@ -0,0 +1,21 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Factories\HasFactory;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Database\Eloquent\SoftDeletes;
8
+use Spatie\Translatable\HasTranslations;
9
+
10
+class AlbumCategory extends Model
11
+{
12
+    use HasFactory, SoftDeletes, HasTranslations;
13
+
14
+    protected $guarded = ['id'];
15
+
16
+    public $translatable = ['name'];
17
+
18
+    public function albums(){
19
+        return $this->hasMany(Album::class);
20
+    }
21
+}

+ 30
- 0
database/migrations/2025_11_28_043724_create_album_categories_table.php View File

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('album_categories', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->string('name');
17
+            $table->unsignedInteger('order')->default(0)->comment("排序");
18
+            $table->timestamps();
19
+            $table->softDeletes();
20
+        });
21
+    }
22
+
23
+    /**
24
+     * Reverse the migrations.
25
+     */
26
+    public function down(): void
27
+    {
28
+        Schema::dropIfExists('album_categories');
29
+    }
30
+};

+ 46
- 0
database/migrations/2025_11_28_043727_create_albums_table.php View File

@@ -0,0 +1,46 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::disableForeignKeyConstraints();
15
+
16
+        Schema::create('albums', function (Blueprint $table) {
17
+            $table->id();
18
+            $table->unsignedBigInteger('album_category_id')->index();
19
+            $table->foreign('album_category_id')->references('id')->on('album_categories')->comment('類別');
20
+            $table->string('news_img_pc')->comment('列表圖 PC');
21
+            $table->string('news_img_mobile')->comment('列表圖 MOBILE');
22
+            $table->string('title')->comment("標題");
23
+            $table->dateTime('post_date')->comment("文章發布日");
24
+            $table->unsignedTinyInteger('upload_type')->comment('上傳類型 1.url 2.upload files');
25
+            $table->string('link')->comment('連結網址');
26
+            $table->string('meta_img')->nullable()->comment('seo image');
27
+            $table->string("meta_title")->comment("seo title");
28
+            $table->string("meta_description")->comment("seo description");
29
+            $table->boolean('on_top')->default(0)->comment("置頂");
30
+            $table->boolean('visible')->default(1)->comment("顯示/隱藏");
31
+            $table->unsignedInteger('order')->default(0)->comment("排序");
32
+            $table->timestamps();
33
+            $table->softDeletes();
34
+        });
35
+
36
+        Schema::enableForeignKeyConstraints();
37
+    }
38
+
39
+    /**
40
+     * Reverse the migrations.
41
+     */
42
+    public function down(): void
43
+    {
44
+        Schema::dropIfExists('albums');
45
+    }
46
+};

+ 3
- 0
routes/api.php View File

@@ -37,6 +37,9 @@ Route::prefix('{locale}')->group(function (){
37 37
         Route::get('/{id}', [ProjectController::class, 'detail'])->whereIn('locale', ["tw", "en"])->where('id', '[0-9]+');
38 38
         Route::get('/badges', [ProjectController::class, 'badges']);
39 39
     });
40
+    Route::prefix('album')->group(function (){
41
+        Route::get('/list', [AlbumController::class, 'list']);
42
+    });
40 43
 
41 44
     Route::prefix("esg")->group(function () {
42 45
         Route::get("/histories", [EsgController::class, 'histories']);