Browse Source

ESG Backend & Apis

parent
commit
b9f489b4bd

+ 107
- 0
app/Filament/Pages/EsgMainPage.php View File

@@ -0,0 +1,107 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages;
4
+
5
+use App\Models\EsgMain;
6
+use Filament\Forms\Components\Actions\Action;
7
+use Filament\Forms\Components\FileUpload;
8
+use Filament\Forms\Components\Tabs;
9
+use Filament\Forms\Components\Tabs\Tab;
10
+use Filament\Forms\Components\Textarea;
11
+use Filament\Forms\Components\TextInput;
12
+use Filament\Forms\Concerns\InteractsWithForms;
13
+use Filament\Forms\Form;
14
+use Filament\Notifications\Notification;
15
+use Filament\Pages\Page;
16
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
17
+
18
+class EsgMainPage extends Page
19
+{
20
+    use InteractsWithForms;
21
+
22
+    protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
23
+    protected static ?string $navigationLabel = 'ESG 主要設定';
24
+    protected static ?string $title = 'ESG 主要設定';
25
+    protected static string $view = 'filament.pages.esg-main';
26
+    protected static ?string $navigationGroup = 'ESG 上稿內容管理';
27
+    protected static ?int $navigationSort = 99;
28
+
29
+    public ?array $data = [];
30
+    protected ?EsgMain $esgMain = null;
31
+
32
+    public function mount(): void
33
+    {
34
+        $this->esgMain = EsgMain::getMain();
35
+        $esgMainArray = $this->esgMain->safeToArray();
36
+        \Log::info("輸出", $esgMainArray);
37
+        $this->form->fill($esgMainArray);
38
+    }
39
+
40
+    public function form(Form $form): Form
41
+    {
42
+        return $form
43
+            ->schema([
44
+                Tabs::make('設定')->tabs([
45
+                    Tab::make('基本資訊')->schema([
46
+                        Translate::make()->schema(fn (string $locale) => [
47
+                            TextInput::make('title')
48
+                            ->label("標題"),
49
+                            Textarea::make('description')
50
+                        ])->locales(["zh_TW", "en"])
51
+                        ->columnSpanFull(),
52
+                        FileUpload::make('banner_pc')->label("Banner (PC)")
53
+                        ->disk("public")
54
+                        ->directory("esg"),
55
+                        FileUpload::make('banner_mobile')->label("Banner (Mobile)")
56
+                        ->disk("public")
57
+                        ->directory("esg"),
58
+                    ]),
59
+                    Tab::make('SEO 設定')->schema([
60
+                        Translate::make()->schema(fn (string $locale) => [
61
+                            TextInput::make('meta_title'),
62
+                            TextInput::make('meta_keyword'),
63
+                            Textarea::make('meta_description'),
64
+                        ])->locales(["zh_TW", "en"])
65
+                        ->columnSpanFull(),
66
+                        FileUpload::make('og_img')
67
+                        ->disk("public")
68
+                        ->directory("esg"),
69
+                    ]),
70
+                ])
71
+            ])
72
+            ->statePath('data');
73
+    }
74
+
75
+    public function save(): void
76
+    {
77
+        try {
78
+            $data = $this->form->getState();
79
+
80
+            $settings = EsgMain::getSettings();
81
+            $settings->update($data);
82
+
83
+            Notification::make()
84
+                ->title('設定已儲存')
85
+                ->success()
86
+                ->send();
87
+
88
+        } catch (\Exception $e) {
89
+            Notification::make()
90
+                ->title('儲存失敗')
91
+                ->body($e->getMessage())
92
+                ->danger()
93
+                ->send();
94
+        }
95
+    }
96
+
97
+    protected function getFormActions(): array
98
+    {
99
+        return [
100
+            Action::make('save')
101
+                ->label('儲存設定')
102
+                ->action('save')
103
+                ->color('primary'),
104
+        ];
105
+    }
106
+
107
+}

+ 380
- 0
app/Filament/Resources/EsgResource.php View File

@@ -0,0 +1,380 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\EsgResource\Pages;
6
+use App\Models\Esg;
7
+use Filament\Forms\Components\FileUpload;
8
+use Filament\Forms\Components\Grid;
9
+use Filament\Forms\Components\Group;
10
+use Filament\Forms\Components\Placeholder;
11
+use Filament\Forms\Components\Radio;
12
+use Filament\Forms\Components\Repeater;
13
+use Filament\Forms\Components\RichEditor;
14
+use Filament\Forms\Components\Section;
15
+use Filament\Forms\Components\Select;
16
+use Filament\Forms\Components\Tabs;
17
+use Filament\Forms\Components\Tabs\Tab;
18
+use Filament\Forms\Components\Textarea;
19
+use Filament\Forms\Components\TextInput;
20
+use Filament\Forms\Components\Toggle;
21
+use Filament\Forms\Form;
22
+use Filament\Forms\Get;
23
+use Filament\Resources\Resource;
24
+use Filament\Tables;
25
+use Filament\Tables\Columns\TextColumn;
26
+use Filament\Tables\Table;
27
+use Illuminate\Database\Eloquent\Builder;
28
+use Illuminate\Database\Eloquent\SoftDeletingScope;
29
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
30
+use Str;
31
+
32
+class EsgResource extends Resource
33
+{
34
+    protected static ?string $model = Esg::class;
35
+
36
+    protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
37
+    protected static ?string $modelLabel = "ESG 管理";
38
+    protected static ?string $navigationGroup = 'ESG 上稿內容管理';
39
+    protected static ?string $navigationLabel = "ESG 管理";
40
+
41
+    public static function form(Form $form): Form
42
+    {
43
+        return $form
44
+            ->schema([
45
+                //
46
+                Tabs::make("")->tabs([
47
+                    Tab::make('基本資訊')->schema([
48
+                        TextInput::make('keyword')->label("關聯字詞"),
49
+                        Translate::make()->schema(fn (string $locale) => [
50
+                            TextInput::make('title')
51
+                            ->label("標題")
52
+                            ->columnSpan(1),
53
+                        ])->locales(["zh_TW", "en"])
54
+                        ->columnSpanFull()->columns(3),
55
+                    ]),
56
+                    Tab::make('Banner 設定')->schema([
57
+                        FileUpload::make('banner_pc')->label("Banner (PC)")
58
+                        ->disk("public")
59
+                        // ->helperText('建議寬高限制為:1280*720px,檔案大小限制為1M以下')->maxSize('1024')
60
+                        ->directory("esg"),
61
+                        FileUpload::make('banner_mobile')->label("Banner (Mobile)")
62
+                        ->disk("public")
63
+                        // ->helperText('建議寬高限制為:600*896px,檔案大小限制為1M以下')->maxSize('1024')
64
+                        ->directory("esg"),
65
+                        Translate::make()->schema(fn (string $locale) => [
66
+                            TextInput::make('banner_alt')
67
+                            ->label("Banner 圖片註釋"),
68
+                            Textarea::make("description")->rows(5)->columnSpanFull()->label("短文"),
69
+                        ])->locales(["zh_TW", "en"])
70
+                        ->columnSpanFull(),
71
+                    ]),
72
+                    Tab::make("段落設計")->schema([
73
+                        //1: 純文字 2:區塊文字 3:表格 4:影片 or 圖片 5:左右圖文
74
+                        Repeater::make("paragraphs")->schema([
75
+                            TextInput::make('item_key')
76
+                                ->default(fn () => Str::random())
77
+                                ->hidden()
78
+                                ->afterStateHydrated(function (TextInput $component, $state) {
79
+                                    if (empty($state)) {
80
+                                        $component->state(Str::random());
81
+                                    }
82
+                                }),
83
+                            Radio::make("type")->options([
84
+                                1 => "純文字",
85
+                                2 => "區塊文字",
86
+                                3 => "表格",
87
+                                4 => "圖片",
88
+                            ])->label("")->default(1)->Live(),
89
+                            Group::make()->schema([
90
+                                Translate::make()->schema(fn (string $locale) => [
91
+                                    RichEditor::make('content.text_content')
92
+                                    ->fileAttachmentsDirectory('attachments')
93
+                                    ->fileAttachmentsVisibility('private')
94
+                                    ->disableToolbarButtons(['attachFiles']),
95
+                                ])
96
+                                ->locales(["zh_TW", "en"])
97
+                                ->id(fn ($get) => "para_text_" . $get('item_key')),
98
+                            ])->visible(fn (Get $get):bool => $get("type") == 1),
99
+                            Group::make()->schema([
100
+                                Section::make('區塊文字設定')->schema([
101
+                                    // // 佈局設定
102
+                                    // Radio::make('layout_type')->label('佈局方式')->options([
103
+                                    //     'vertical' => '垂直排列',
104
+                                    //     'horizontal' => '水平排列',
105
+                                    //     'grid' => '網格排列',
106
+                                    // ])->default('vertical')->Live(),
107
+                                    // 文字區塊重複器
108
+                                    Repeater::make("content.text_blocks")->schema([
109
+                                        TextInput::make('block_item_key')
110
+                                            ->default(fn () => Str::random())
111
+                                            ->hidden()
112
+                                            ->afterStateHydrated(function (TextInput $component, $state) {
113
+                                                if (empty($state)) {
114
+                                                    $component->state(Str::random());
115
+                                                }
116
+                                            }),
117
+                                        // 多語系內容
118
+                                        Translate::make()->schema(fn (string $locale) => [
119
+                                            // TextInput::make('block_title')
120
+                                            //     ->label("區塊標題")
121
+                                            //     ->maxLength(200)
122
+                                            //     ->visible(fn (Get $get) => in_array($get('block_type'), ['title', 'subtitle', 'highlight'])),
123
+                                            RichEditor::make('block_content')
124
+                                                ->disableToolbarButtons(['attachFiles'])
125
+                                                ->required(),
126
+                                        ])
127
+                                        ->locales(["zh_TW", "en"])
128
+                                        ->id(fn ($get) => "text_block_" . $get('block_item_key')),
129
+
130
+                                    ])
131
+                                    ->label("")
132
+                                    ->addActionLabel('增加區塊')
133
+                                    ->orderColumn('order')
134
+                                    ->reorderableWithButtons()
135
+                                    ->cloneable()
136
+                                    ->minItems(1)
137
+                                    ->maxItems(20)
138
+                                    ->defaultItems(1),
139
+                                ]),
140
+                            ])->visible(fn (Get $get):bool => $get('type') == 2),
141
+                            Group::make()->schema([
142
+                                Section::make('表格設定')->schema([
143
+                                    Radio::make('content.is_card')
144
+                                            ->label('手機板卡片顯示')
145
+                                            ->options([
146
+                                                1 => '是',  // 改為數字鍵值
147
+                                                0 => '否'
148
+                                            ])
149
+                                            ->default(2)
150
+                                            ->inline(),
151
+                                    // 表格基本資訊
152
+                                    // Translate::make()->schema(fn (string $locale) => [
153
+                                    //     TextInput::make('content.table_title')
154
+                                    //         ->label("表格標題")
155
+                                    //         ->maxLength(200),
156
+
157
+                                    //     Textarea::make('content.table_description')
158
+                                    //         ->label("表格說明")
159
+                                    //         ->rows(3)
160
+                                    //         ->maxLength(500),
161
+                                    // ])->locales(["zh_TW", "en"])
162
+                                    // ->id(fn ($get) => "table_info_" . $get('item_key')),
163
+                                    // 表格資料
164
+                                    Placeholder::make('table_builder_info')
165
+                                        ->content('請先設定表格欄數,然後填入表格資料'),
166
+                                    Grid::make(3)->schema([
167
+                                        Radio::make('content.column_count')
168
+                                            ->label('欄數')
169
+                                            ->options([
170
+                                                2 => '2欄',  // 改為數字鍵值
171
+                                                3 => '3欄',
172
+                                                4 => '4欄'
173
+                                            ])
174
+                                            ->default(2)
175
+                                            ->inline()
176
+                                            ->live()
177
+                                            ->reactive(), // ✅ 添加 reactive()
178
+                                        // Radio::make('content.is_card')
179
+                                        //     ->label('是否卡片呈現')
180
+                                        //     ->options([
181
+                                        //         1 => '是',  // 改為數字鍵值
182
+                                        //         0 => '否',
183
+                                        //     ])
184
+                                        //     ->default(1)
185
+                                        //     ->inline()
186
+                                        //     ->required()
187
+                                    ]),
188
+                                    Translate::make()->schema(fn (string $locale) => [
189
+                                        Grid::make(2)  // 改用 Grid 來支援動態 columns
190
+                                            ->schema([
191
+                                                TextInput::make('content.head1')->label('標題第1欄')->required(),
192
+                                                Radio::make('content.head_align1')
193
+                                                    ->label('對齊')
194
+                                                    ->options([
195
+                                                        1 => '置左',  // 改為數字鍵值
196
+                                                        2 => '置中',
197
+                                                        3 => '置右'
198
+                                                    ])
199
+                                                    ->default(1),
200
+                                                TextInput::make('content.head2')->label('標題第2欄')->required(),
201
+                                                Radio::make('content.head_align2')
202
+                                                    ->label('對齊')
203
+                                                    ->options([
204
+                                                        1 => '置左',  // 改為數字鍵值
205
+                                                        2 => '置中',
206
+                                                        3 => '置右'
207
+                                                    ])
208
+                                                    ->default(1),
209
+                                                TextInput::make('content.head3')->label('標題第3欄')
210
+                                                    ->visible(fn (Get $get) => intval($get('content.column_count')) >= 3)->columnSpan(1),
211
+                                                    Radio::make('content.head_align3')
212
+                                                        ->label('對齊')
213
+                                                        ->options([
214
+                                                            1 => '置左',  // 改為數字鍵值
215
+                                                            2 => '置中',
216
+                                                            3 => '置右'
217
+                                                        ])
218
+                                                        ->default(1)
219
+                                                        ->visible(fn (Get $get) => intval($get('content.column_count')) >= 3)->columnSpan(1),
220
+                                                TextInput::make('content.head4')->label('標題第4欄')
221
+                                                    ->visible(fn (Get $get) => intval($get('content.column_count')) >= 4)->columnSpan(1),
222
+                                                    Radio::make('content.head_align4')
223
+                                                        ->label('對齊')
224
+                                                        ->options([
225
+                                                            1 => '置左',  // 改為數字鍵值
226
+                                                            2 => '置中',
227
+                                                            3 => '置右'
228
+                                                        ])
229
+                                                        ->default(1)
230
+                                                        ->visible(fn (Get $get) => intval($get('content.column_count')) >= 4)->columnSpan(1),
231
+                                            ])
232
+                                            ->reactive()  // 加入響應式
233
+                                    ])->locales(["zh_TW", "en"]),
234
+                                    Repeater::make('content.simple_table_rows')
235
+                                        ->label('表格資料')
236
+                                        ->schema([
237
+                                            TextInput::make('simple_table_rows_key')
238
+                                                ->default(fn () => Str::random())
239
+                                                ->hidden()
240
+                                                ->afterStateHydrated(function (TextInput $component, $state) {
241
+                                                    if (empty($state)) {
242
+                                                        $component->state(Str::random());
243
+                                                    }
244
+                                                }),
245
+                                            Translate::make()->schema(fn (string $locale) => [
246
+                                                Grid::make(1)  // 改用 Grid 來支援動態 columns
247
+                                                    ->schema([
248
+                                                        Radio::make('align1')
249
+                                                            ->label('對齊')
250
+                                                            ->options([
251
+                                                                1 => '置左',  // 改為數字鍵值
252
+                                                                2 => '置中',
253
+                                                                3 => '置右'
254
+                                                            ])
255
+                                                        ->default(1),
256
+                                                        RichEditor::make('col1')
257
+                                                            ->label('第1欄')
258
+                                                            ->disableToolbarButtons(['attachFiles']),
259
+                                                        // TextInput::make('col2')->label('第2欄')->required(),
260
+                                                        Radio::make('align2')
261
+                                                            ->label('對齊')
262
+                                                            ->options([
263
+                                                                1 => '置左',  // 改為數字鍵值
264
+                                                                2 => '置中',
265
+                                                                3 => '置右'
266
+                                                            ])
267
+                                                            ->default(1),
268
+                                                        RichEditor::make('col2')
269
+                                                            ->label('第2欄')
270
+                                                            ->disableToolbarButtons(['attachFiles']),
271
+                                                        Radio::make('align3')
272
+                                                            ->label('對齊')
273
+                                                            ->options([
274
+                                                                1 => '置左',  // 改為數字鍵值
275
+                                                                2 => '置中',
276
+                                                                3 => '置右'
277
+                                                            ])
278
+                                                            ->default(1)
279
+                                                            ->visible(fn (Get $get) => intval($get('../../../content.column_count')) >= 3),
280
+                                                        RichEditor::make('col3')
281
+                                                            ->label('第3欄')
282
+                                                            ->disableToolbarButtons(['attachFiles'])
283
+                                                            ->visible(fn (Get $get) => intval($get('../../../content.column_count')) >= 3),
284
+                                                            Radio::make('align4')
285
+                                                                ->label('對齊')
286
+                                                                ->options([
287
+                                                                    1 => '置左',  // 改為數字鍵值
288
+                                                                    2 => '置中',
289
+                                                                    3 => '置右'
290
+                                                                ])
291
+                                                                ->default(1)
292
+                                                                ->visible(fn (Get $get) => intval($get('../../../content.column_count')) >= 4),
293
+                                                            RichEditor::make('col4')
294
+                                                                ->label('第4欄')
295
+                                                                ->disableToolbarButtons(['attachFiles'])
296
+                                                                ->visible(fn (Get $get) => intval($get('../../../content.column_count')) >= 4),
297
+                                                    ])
298
+                                                    ->reactive()  // 加入響應式
299
+                                            ])->locales(["zh_TW", "en"]),
300
+                                        ])
301
+                                        ->id(fn ($get) => "simple_table_" . $get('simple_table_key'))
302
+                                        ->addActionLabel('新增列')
303
+                                        ->reorderableWithButtons()
304
+                                        ->minItems(1)
305
+                                ]),
306
+                            ])->visible(fn (Get $get):bool => $get('type') == 3),
307
+                            Group::make()->schema([
308
+                                Section::make('')->schema([
309
+                                    Repeater::make("content.multiple_images")->schema([
310
+                                        TextInput::make('para_img_item_key')
311
+                                            ->default(fn () => Str::random())
312
+                                            ->hidden()
313
+                                            ->afterStateHydrated(function (TextInput $component, $state) {
314
+                                                if (empty($state)) {
315
+                                                    $component->state(Str::random());
316
+                                                }
317
+                                            }),
318
+                                        Translate::make()->schema(fn (string $locale) => [
319
+                                            TextInput::make('image_alt')->label("圖片註文"),
320
+                                        ])->locales(["zh_TW", "en"])
321
+                                        ->id(fn ($get) => "para_img_mul_" . $get('para_img_item_key')),
322
+                                        FileUpload::make('image_url')->label("")->disk("public")
323
+                                        // ->helperText('建議寬高限制為:1080*675px,檔案大小限制為1M以下')->maxSize('1024')
324
+                                        ->directory("esg/paragraphPhoto")
325
+                                        ->maxFiles(10),
326
+                                    ])
327
+                                    ->addActionLabel('新增')
328
+                                    ->label("")
329
+                                    ->orderColumn('order')
330
+                                ])
331
+                            ])->visible(fn (Get $get):bool => $get("type") == 4),
332
+                        ])
333
+                        ->relationship("paragraphs")
334
+                        ->addActionLabel('新增段落')
335
+                        ->collapsible()
336
+                        ->reorderableWithButtons()
337
+                        ->orderColumn('order')
338
+                        ->cloneable()
339
+                    ])->columnSpanFull(),
340
+                ])
341
+                ->columnSpanFull()
342
+            ]);
343
+    }
344
+
345
+    public static function table(Table $table): Table
346
+    {
347
+        return $table
348
+            ->columns([
349
+                //
350
+                TextColumn::make("title")->label("標題"),
351
+            ])
352
+            ->filters([
353
+                //
354
+            ])
355
+            ->actions([
356
+                Tables\Actions\EditAction::make(),
357
+            ])
358
+            ->bulkActions([
359
+                Tables\Actions\BulkActionGroup::make([
360
+                    Tables\Actions\DeleteBulkAction::make(),
361
+                ]),
362
+            ]);
363
+    }
364
+
365
+    public static function getRelations(): array
366
+    {
367
+        return [
368
+            //
369
+        ];
370
+    }
371
+
372
+    public static function getPages(): array
373
+    {
374
+        return [
375
+            'index' => Pages\ListEsg::route('/'),
376
+            'create' => Pages\CreateEsg::route('/create'),
377
+            'edit' => Pages\EditEsg::route('/{record}/edit'),
378
+        ];
379
+    }
380
+}

+ 43
- 0
app/Filament/Resources/EsgResource/Pages/CreateEsg.php View File

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\EsgResource\Pages;
4
+
5
+use App\Filament\Resources\EsgResource;
6
+use App\Traits\HandlesEsgParagraphs;
7
+use Filament\Actions;
8
+use Filament\Resources\Pages\CreateRecord;
9
+use Illuminate\Database\Eloquent\Model;
10
+
11
+class CreateEsg extends CreateRecord
12
+{
13
+    use HandlesEsgParagraphs;
14
+    protected static string $resource = EsgResource::class;
15
+
16
+    protected function getRedirectUrl(): string
17
+    {
18
+        return $this->getResource()::getUrl('index');
19
+    }
20
+    protected function handleRecordCreation(array $data): Model
21
+    {
22
+        \Log::info("=== 開始建立流程 ===");
23
+
24
+        // ✅ 使用 Trait 方法處理表單中的 paragraphs
25
+        $processedParagraphs = $this->processFormParagraphs();
26
+
27
+        if ($processedParagraphs) {
28
+            \Log::info("預處理後的 paragraphs:", $processedParagraphs);
29
+        }
30
+
31
+        // 建立主記錄
32
+        $record = static::getModel()::create($data);
33
+        \Log::info("主記錄已建立:", ['id' => $record->id]);
34
+
35
+        // ✅ 使用 Trait 方法建立 paragraphs 關聯
36
+        if ($processedParagraphs) {
37
+            $this->createParagraphs($record, $processedParagraphs);
38
+        }
39
+
40
+        return $record->refresh();
41
+    }
42
+
43
+}

+ 48
- 0
app/Filament/Resources/EsgResource/Pages/EditEsg.php View File

@@ -0,0 +1,48 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\EsgResource\Pages;
4
+
5
+use App\Filament\Resources\EsgResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+use Illuminate\Database\Eloquent\Model;
9
+use App\Traits\HandlesEsgParagraphs;
10
+
11
+class EditEsg extends EditRecord
12
+{
13
+    use HandlesEsgParagraphs;
14
+    protected static string $resource = EsgResource::class;
15
+
16
+    protected function getRedirectUrl(): string
17
+    {
18
+        return $this->getResource()::getUrl('index');
19
+    }
20
+
21
+    /**
22
+     * 在填充表單前處理資料 - 手動載入關聯資料
23
+     */
24
+    protected function mutateFormDataBeforeFill(array $data): array
25
+    {
26
+
27
+        // ✅ 使用 Trait 方法載入並格式化 paragraphs
28
+        $data['paragraphs'] = $this->loadAndFormatParagraphs($this->record);
29
+
30
+        return $data;
31
+    }
32
+
33
+    /**
34
+     * 儲存時的處理
35
+     */
36
+    protected function handleRecordUpdate(Model $record, array $data): Model
37
+    {
38
+        // ✅ 使用 Trait 方法處理表單中的 paragraphs
39
+        $processedParagraphs = $this->processFormParagraphs();
40
+        // 更新主記錄
41
+        $record->update($data);
42
+        // ✅ 使用 Trait 方法處理 paragraphs 關聯
43
+        if ($processedParagraphs) {
44
+            $this->updateParagraphs($record, $processedParagraphs);
45
+        }
46
+        return $record->refresh();
47
+    }
48
+}

+ 19
- 0
app/Filament/Resources/EsgResource/Pages/ListEsg.php View File

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

+ 143
- 0
app/Http/Controllers/Api/EsgController.php View File

@@ -0,0 +1,143 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\Esg;
7
+use App\Models\EsgMain;
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
+use App\Http\Helper\Helper;
14
+use Illuminate\Support\Facades\Storage;
15
+
16
+/**
17
+ * @group Lottery Prize
18
+ */
19
+class EsgController extends Controller
20
+{
21
+    public function __construct(
22
+    )
23
+    {
24
+    }
25
+
26
+    public function main($locate = 'tw')
27
+    {
28
+        $data = [];
29
+        $esgMain = EsgMain::first();
30
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
31
+        $data = [
32
+            "meta" => [
33
+                "meta_title" => $esgMain->meta_title,
34
+                "meta_keyword" => $esgMain->meta_keyword,
35
+                "meta_description" => $esgMain->meta_description,
36
+                "og_img" => $esgMain->og_img
37
+            ],
38
+            "img_pc" => $esgMain->img_pc_url,
39
+            "img_mobile" => $esgMain->img_mobile_url,
40
+            "title" => $esgMain->getTranslation("title", $locate, false),
41
+            "description" => $esgMain->getTranslation("description", $locate, false),
42
+        ];
43
+        $esgs = Esg::select("title", "keyword")->get();
44
+        foreach($esgs as $esg){
45
+            $data["tab_list"][] = [
46
+                "text" => $esg->getTranslation("title", $locate, false),
47
+                "key" => $esg->keyword
48
+            ];
49
+        }
50
+        return response()->json(["data" => $data], 200);
51
+    }
52
+
53
+    public function esgContent($locate, $keyword){
54
+
55
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
56
+        if (!$esg = Esg::getContentByKeyword($keyword)) {
57
+            return response()->json([
58
+                'status' => "fail",
59
+                'message' => '查無資料'
60
+            ]);
61
+        }
62
+
63
+        $data = [
64
+            "banner" => [
65
+                "img_pc" => $esg->banner_pc_url,
66
+                "img_mobile" => $esg->banner_mobile_url,
67
+                "img_alt" => $esg->img_alt,
68
+                "text" => $esg->description ?? "",
69
+            ],
70
+            "paragraphs" => $this->prepareOutputContent($esg->paragraphs->toArray(), $locate)
71
+        ];
72
+
73
+        return response()->json(["data" => $data], 200);
74
+    }
75
+
76
+    private function prepareOutputContent($paragraphs, $locate): array
77
+    {
78
+        $output = [];
79
+        // 調試關聯資料
80
+        if($paragraphs){
81
+            foreach($paragraphs as $paragraph){
82
+                $content = $paragraph["content"];
83
+                switch ($paragraph["type"]){
84
+                    case "1":
85
+                        $output[] = [
86
+                            "type" => "text",
87
+                            "content" => $content["text_content"][$locate]
88
+                        ];
89
+                        break;
90
+                    case "2":
91
+                        $output[] = [
92
+                            "type" => "grid",
93
+                            "columns" => count($content["text_blocks"]),
94
+                            "content" => array_map(function($block) use ($locate) {
95
+                                return [
96
+                                    "type" => "text",
97
+                                    "content" => $block["block_content"][$locate]
98
+                                ];
99
+                            }, $content["text_blocks"])
100
+                        ];
101
+                        break;
102
+                    case "3":
103
+                        $column_count = $content["column_count"];
104
+                        $table_header = [];
105
+                        $table_content = [];
106
+                        for($i=1; $i<=$column_count; $i++){
107
+                            $table_header[] = [
108
+                                "align" => $content["head_align" . strval($i)][$locate],
109
+                                "text" => $content["head" . strval($i)][$locate]
110
+                            ];
111
+                            $table_row_content = [];
112
+                            foreach($content["simple_table_rows"] as $table_row){
113
+                                $table_row_content[] = [
114
+                                    "align" => $table_row["align" . strval($i)][$locate],
115
+                                    "text" => $table_row["col" . strval($i)][$locate]
116
+                                ];
117
+                            }
118
+                            $table_content[] = $table_row_content;
119
+                        }
120
+                        $output[] = [
121
+                            "type" => "table",
122
+                            "is_card" => boolval($content["is_card"]),
123
+                            "header" => $table_header,
124
+                            "body" => $table_content
125
+                        ];
126
+                        break;
127
+                    case "4":
128
+                        $output[] = [
129
+                            "type" => "image",
130
+                            "images" => array_map(function($image) use ($locate) {
131
+                                return [
132
+                                    "image_url" => Storage::url($image["image_url"]),
133
+                                    "alt" => $image["image_alt"][$locate]
134
+                                ];
135
+                            }, $content["multiple_images"])
136
+                        ];
137
+                        break;
138
+                }
139
+            }
140
+        }
141
+        return $output;
142
+    }
143
+}

+ 44
- 0
app/Models/Esg.php View File

@@ -0,0 +1,44 @@
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 Esg extends Model
13
+{
14
+    use HasFactory, SoftDeletes, HasTranslations;
15
+    //
16
+    protected $guarded = ["id"];
17
+    protected $casts = [
18
+        'on_top' => 'boolean'
19
+    ];
20
+    protected $appends = ["banner_pc_url", "banner_mobile_url"];
21
+
22
+    public $translatable = ['title', 'banner_alt', 'description'];
23
+
24
+    public function paragraphs(){
25
+        return $this->hasMany(EsgParagraph::class)->orderBy('order');
26
+    }
27
+    protected function bannerPcUrl(): Attribute
28
+    {
29
+        return Attribute::make(
30
+            get: fn ($value) => is_null($this->banner_pc) ? null :Storage::url($this->banner_pc),
31
+        );
32
+    }
33
+
34
+    protected function bannerMobileUrl(): Attribute
35
+    {
36
+        return Attribute::make(
37
+            get: fn ($value) => is_null($this->banner_mobile) ? null :Storage::url($this->banner_mobile),
38
+        );
39
+    }
40
+
41
+    protected function getContentByKeyword($keyword){
42
+        return $this->where("keyword", $keyword)->with('paragraphs')->first();
43
+    }
44
+}

+ 90
- 0
app/Models/EsgMain.php View File

@@ -0,0 +1,90 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Support\Facades\Storage;
8
+use Spatie\Translatable\HasTranslations;
9
+
10
+class EsgMain extends Model
11
+{
12
+    use HasTranslations;
13
+    protected $guarded = ['id'];
14
+    protected $appends = ["img_pc_url", "og_img_url", "img_mobile_url"];
15
+    public $translatable = ['title', 'description', 'meta_title', 'meta_keyword', 'meta_description'];
16
+    protected function imgPcUrl(): Attribute
17
+    {
18
+        return Attribute::make(
19
+            get: fn ($value) => is_null($this->img_pc) ? null :Storage::url($this->img_pc),
20
+        );
21
+    }
22
+    protected function imgMobileUrl(): Attribute
23
+    {
24
+        return Attribute::make(
25
+            get: fn ($value) => is_null($this->img_mobile) ? null :Storage::url($this->img_mobile),
26
+        );
27
+    }
28
+    protected function ogImgUrl(): Attribute
29
+    {
30
+        return Attribute::make(
31
+            get: fn ($value) => is_null($this->og_img) ? null :Storage::url($this->og_img),
32
+        );
33
+    }
34
+    public static function getMain(): self
35
+    {
36
+        $main = self::first();
37
+        if(empty($main)){
38
+            $main = self::create(self::getDefaultData());
39
+        }
40
+        return $main;
41
+    }
42
+
43
+    /**
44
+     * ✅ 獲取預設設定值
45
+     */
46
+    public static function getDefaultData(): array
47
+    {
48
+        return [
49
+            'title' => [
50
+                'zh_TW' => '預設網站標題',
51
+                'en' => 'Default Site Title'
52
+            ],
53
+            'description' => [
54
+                'zh_TW' => '預設網站描述',
55
+                'en' => 'Default Site Description'
56
+            ],
57
+        ];
58
+    }
59
+
60
+    /**
61
+     * ✅ 安全的 toArray 方法,確保多語系欄位格式正確
62
+     */
63
+    public function safeToArray(): array
64
+    {
65
+        $array = $this->toArray();
66
+
67
+        // 確保多語系欄位是正確的格式
68
+        foreach ($this->translatable as $field) {
69
+            if (isset($array[$field])) {
70
+                // 如果是字串,轉換為多語系格式
71
+                if (is_string($array[$field])) {
72
+                    $array[$field] = [
73
+                        'zh_TW' => $array[$field],
74
+                        'en' => ''
75
+                    ];
76
+                }
77
+                // 確保有必要的語言鍵
78
+                if (is_array($array[$field])) {
79
+                    $array[$field]['zh_TW'] = $array[$field]['zh_TW'] ?? '';
80
+                    $array[$field]['en'] = $array[$field]['en'] ?? '';
81
+                }
82
+            } else {
83
+                // 如果欄位不存在,建立預設結構
84
+                $array[$field] = ['zh_TW' => '', 'en' => ''];
85
+            }
86
+        }
87
+
88
+        return $array;
89
+    }
90
+}

+ 52
- 0
app/Models/EsgParagraph.php View File

@@ -0,0 +1,52 @@
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\Relations\BelongsTo;
9
+use Illuminate\Database\Eloquent\SoftDeletes;
10
+use Illuminate\Support\Facades\Storage;
11
+use Spatie\Translatable\HasTranslations;
12
+
13
+class EsgParagraph extends Model
14
+{
15
+    use HasFactory, HasTranslations;
16
+    //
17
+    protected $fillable = [
18
+        'esg_id',
19
+        'type',
20
+        'order',
21
+        'content',
22
+    ];
23
+
24
+    protected $casts = [
25
+        'content' => 'array', // 自動轉換 JSON
26
+        'type' => 'integer',
27
+        'order' => 'integer'
28
+    ];
29
+
30
+    public function esg(): BelongsTo
31
+    {
32
+        return $this->belongsTo(Esg::class);
33
+    }
34
+
35
+    // ✅ 取得段落類型名稱
36
+    public function getTypeNameAttribute(): string
37
+    {
38
+        return match($this->type) {
39
+            1 => '純文字',
40
+            2 => '區塊文字',
41
+            3 => '表格',
42
+            4 => '圖片',
43
+            default => '未知',
44
+        };
45
+    }
46
+
47
+    // 如果你想要更細緻的控制,可以使用 accessor 和 mutator
48
+    public function getContentAttribute($value)
49
+    {
50
+        return $value ? json_decode($value, true) : [];
51
+    }
52
+}

+ 439
- 0
app/Traits/HandlesEsgParagraphs.php View File

@@ -0,0 +1,439 @@
1
+<?php
2
+// app/Traits/HandlesParagraphs.php
3
+
4
+namespace App\Traits;
5
+
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Support\Str;
8
+
9
+trait HandlesEsgParagraphs
10
+{
11
+    /**
12
+     * 格式化 paragraphs 資料以符合表單結構(載入時使用)
13
+     */
14
+    protected function formatParagraphsForForm(array $paragraphs): array
15
+    {
16
+        $formattedParagraphs = [];
17
+
18
+        foreach ($paragraphs as $paragraph) {
19
+            \Log::info("格式化段落:", $paragraph);
20
+
21
+            $formatted = [
22
+                'id' => $paragraph['id'],
23
+                'type' => $paragraph['type'],
24
+                'order' => $paragraph['order'],
25
+                'content' => $paragraph['content'] ?? [],
26
+            ];
27
+
28
+            // 根據類型進行特殊格式化
29
+            switch ($paragraph['type']) {
30
+                case 1: // 純文字
31
+                    $formatted = $this->formatTextParagraphForForm($formatted);
32
+                    break;
33
+
34
+                case 2: // 區塊文字
35
+                    $formatted = $this->formatBlockParagraphForForm($formatted);
36
+                    break;
37
+
38
+                case 3: // 表格
39
+                    $formatted = $this->formatTableParagraphForForm($formatted);
40
+                    break;
41
+
42
+                case 4: // 圖片
43
+                    $formatted = $this->formatImageParagraphForForm($formatted);
44
+                    break;
45
+            }
46
+
47
+            // 確保有 item_key
48
+            if (!isset($formatted['item_key'])) {
49
+                $formatted['item_key'] = Str::random();
50
+            }
51
+
52
+            $formattedParagraphs[] = $formatted;
53
+
54
+            \Log::info("格式化完成:", $formatted);
55
+        }
56
+
57
+        return $formattedParagraphs;
58
+    }
59
+
60
+    /**
61
+     * 格式化純文字段落(載入時)
62
+     */
63
+    protected function formatTextParagraphForForm(array $paragraph): array
64
+    {
65
+        // 確保 text_content 結構正確
66
+        if (!isset($paragraph['content']['text_content'])) {
67
+            $paragraph['content']['text_content'] = ['zh_TW' => '', 'en' => ''];
68
+        }
69
+
70
+        return $paragraph;
71
+    }
72
+
73
+    /**
74
+     * 格式化區塊文字段落(載入時)
75
+     */
76
+    protected function formatBlockParagraphForForm(array $paragraph): array
77
+    {
78
+        // 確保 text_blocks 結構正確
79
+        if (!isset($paragraph['content']['text_blocks'])) {
80
+            $paragraph['content']['text_blocks'] = [];
81
+        }
82
+
83
+        // 為每個 text_block 添加必要的欄位
84
+        foreach ($paragraph['content']['text_blocks'] as $index => &$block) {
85
+            if (!isset($block['block_item_key'])) {
86
+                $block['block_item_key'] = Str::random();
87
+            }
88
+            if (!isset($block['order'])) {
89
+                $block['order'] = $index + 1;
90
+            }
91
+        }
92
+
93
+        return $paragraph;
94
+    }
95
+
96
+    /**
97
+     * 格式化表格段落(載入時)
98
+     */
99
+    protected function formatTableParagraphForForm(array $paragraph): array
100
+    {
101
+        $content = $paragraph['content'];
102
+
103
+        if (!isset($content['column_count'])) {
104
+            $content['column_count'] = 2;
105
+        }
106
+
107
+        if (!isset($content['table_title'])) {
108
+            $content['table_title'] = ['zh_TW' => '', 'en' => ''];
109
+        }
110
+
111
+        if (!isset($content['table_description'])) {
112
+            $content['table_description'] = ['zh_TW' => '', 'en' => ''];
113
+        }
114
+
115
+        if (!isset($content['simple_table_rows'])) {
116
+            $content['simple_table_rows'] = [];
117
+        }
118
+
119
+        // 確保表頭資料結構
120
+        $columnCount = $content['column_count'];
121
+        for ($i = 1; $i <= $columnCount; $i++) {
122
+            if (!isset($content["head{$i}"])) {
123
+                $content["head{$i}"] = ['zh_TW' => '', 'en' => ''];
124
+            }
125
+        }
126
+
127
+        $paragraph['content'] = $content;
128
+
129
+        return $paragraph;
130
+    }
131
+
132
+    /**
133
+     * 格式化圖片段落(載入時)
134
+     */
135
+    protected function formatImageParagraphForForm(array $paragraph): array
136
+    {
137
+        // 確保 multiple_images 結構正確
138
+        if (!isset($paragraph['content']['multiple_images'])) {
139
+            $paragraph['content']['multiple_images'] = [];
140
+        }
141
+
142
+        // 為每個圖片添加必要的欄位
143
+        foreach ($paragraph['content']['multiple_images'] as $index => &$image) {
144
+            if (!isset($image['para_img_item_key'])) {
145
+                $image['para_img_item_key'] = Str::random();
146
+            }
147
+            if (!isset($image['order'])) {
148
+                $image['order'] = $index + 1;
149
+            }
150
+        }
151
+
152
+        return $paragraph;
153
+    }
154
+
155
+    /**
156
+     * 預處理 paragraphs 資料(儲存前使用)
157
+     */
158
+    protected function preprocessParagraphs(array $paragraphs): array
159
+    {
160
+        \Log::info("=== 開始預處理 Paragraphs ===");
161
+
162
+        foreach ($paragraphs as $index => &$paragraph) {
163
+            \Log::info("處理段落 {$index}:", $paragraph);
164
+
165
+            // 確保基本欄位存在
166
+            $paragraph['content'] = $paragraph['content'] ?? [];
167
+            $paragraph['order'] = $paragraph['order'] ?? ($index + 1);
168
+
169
+            // 根據類型進行特殊處理
170
+            switch ($paragraph['type']) {
171
+                case 1: // 純文字
172
+                    $paragraph = $this->preprocessTextParagraph($paragraph);
173
+                    break;
174
+
175
+                case 2: // 區塊文字
176
+                    $paragraph = $this->preprocessBlockParagraph($paragraph);
177
+                    break;
178
+
179
+                case 3: // 表格
180
+                    $paragraph = $this->preprocessTableParagraph($paragraph);
181
+                    break;
182
+
183
+                case 4: // 圖片
184
+                    $paragraph = $this->preprocessImageParagraph($paragraph);
185
+                    break;
186
+            }
187
+
188
+            // 添加處理時間戳
189
+            $paragraph['content']['processed_at'] = now()->toISOString();
190
+
191
+            \Log::info("段落 {$index} 處理完成:", $paragraph);
192
+        }
193
+
194
+        return $paragraphs;
195
+    }
196
+
197
+    /**
198
+     * 處理純文字段落(儲存前)
199
+     */
200
+    protected function preprocessTextParagraph(array $paragraph): array
201
+    {
202
+        \Log::info("處理純文字段落");
203
+
204
+        // 確保文字內容結構正確
205
+        if (!isset($paragraph['content']['text_content'])) {
206
+            $paragraph['content']['text_content'] = ['zh_TW' => '', 'en' => ''];
207
+        }
208
+
209
+        // 清理 HTML 標籤(如果需要)
210
+        foreach ($paragraph['content']['text_content'] as $locale => $content) {
211
+            $paragraph['content']['text_content'][$locale] = $this->cleanHtml($content);
212
+        }
213
+
214
+        return $paragraph;
215
+    }
216
+
217
+    /**
218
+     * 處理區塊文字段落(儲存前)
219
+     */
220
+    protected function preprocessBlockParagraph(array $paragraph): array
221
+    {
222
+        \Log::info("處理區塊文字段落");
223
+
224
+        // 確保有 text_blocks
225
+        if (!isset($paragraph['content']['text_blocks'])) {
226
+            $paragraph['content']['text_blocks'] = [];
227
+        }
228
+
229
+        // 處理每個文字區塊
230
+        foreach ($paragraph['content']['text_blocks'] as $index => &$block) {
231
+            // 確保區塊有必要的欄位
232
+            $block['block_item_key'] = $block['block_item_key'] ?? Str::random();
233
+            $block['order'] = $block['order'] ?? ($index + 1);
234
+
235
+            // 清理區塊內容
236
+            if (isset($block['block_content'])) {
237
+                foreach ($block['block_content'] as $locale => $content) {
238
+                    $block['block_content'][$locale] = $this->cleanHtml($content);
239
+                }
240
+            }
241
+        }
242
+
243
+        return $paragraph;
244
+    }
245
+
246
+    /**
247
+     * 處理表格段落(儲存前)
248
+     */
249
+    protected function preprocessTableParagraph(array $paragraph): array
250
+    {
251
+        \Log::info("處理表格段落");
252
+
253
+        // 確保欄數設定
254
+        $columnCount = $paragraph['content']['column_count'] ?? 2;
255
+
256
+        // 確保表頭存在
257
+        if (!isset($paragraph['content']['headers'])) {
258
+            $paragraph['content']['headers'] = ['zh_TW' => [], 'en' => []];
259
+        }
260
+
261
+        // 確保表格資料存在
262
+        if (!isset($paragraph['content']['simple_table_rows'])) {
263
+            $paragraph['content']['simple_table_rows'] = [];
264
+        }
265
+
266
+        // 處理表格資料,確保欄數一致
267
+        foreach ($paragraph['content']['simple_table_rows'] as $rowIndex => &$row) {
268
+            foreach (['zh_TW', 'en'] as $locale) {
269
+                if (!isset($row[$locale])) {
270
+                    $row[$locale] = [];
271
+                }
272
+
273
+                // 確保每行都有正確的欄數
274
+                for ($i = 1; $i <= $columnCount; $i++) {
275
+                    if (!isset($row[$locale]["col{$i}"])) {
276
+                        $row[$locale]["col{$i}"] = '';
277
+                    }
278
+                }
279
+
280
+                // 移除多餘的欄位
281
+                for ($i = $columnCount + 1; $i <= 4; $i++) {
282
+                    unset($row[$locale]["col{$i}"]);
283
+                }
284
+            }
285
+        }
286
+
287
+        return $paragraph;
288
+    }
289
+
290
+    /**
291
+     * 處理圖片段落(儲存前)
292
+     */
293
+    protected function preprocessImageParagraph(array $paragraph): array
294
+    {
295
+        \Log::info("處理圖片段落");
296
+
297
+        // 確保圖片陣列存在
298
+        if (!isset($paragraph['content']['multiple_images'])) {
299
+            $paragraph['content']['multiple_images'] = [];
300
+        }
301
+
302
+        // 處理每個圖片
303
+        foreach ($paragraph['content']['multiple_images'] as $index => &$image) {
304
+            $image['para_img_item_key'] = $image['para_img_item_key'] ?? Str::random();
305
+            $image['order'] = $image['order'] ?? ($index + 1);
306
+
307
+            // 驗證圖片檔案是否存在
308
+            if (isset($image['image_url'])) {
309
+                // 這裡可以添加圖片驗證邏輯
310
+            }
311
+        }
312
+
313
+        return $paragraph;
314
+    }
315
+
316
+    /**
317
+     * 更新 paragraphs 關聯(編輯時使用)
318
+     */
319
+    protected function updateParagraphs(Model $record, array $paragraphs): void
320
+    {
321
+        \Log::info("=== 開始更新 Paragraphs 關聯 ===");
322
+
323
+        // 獲取現有的段落 IDs
324
+        $existingIds = collect($paragraphs)
325
+            ->pluck('id')
326
+            ->filter()
327
+            ->values()
328
+            ->toArray();
329
+
330
+        // 刪除不在列表中的段落
331
+        $record->paragraphs()
332
+            ->whereNotIn('id', $existingIds)
333
+            ->delete();
334
+
335
+        \Log::info("已刪除不存在的段落");
336
+
337
+        // 更新或建立段落
338
+        foreach ($paragraphs as $paragraphData) {
339
+            if (isset($paragraphData['id'])) {
340
+                // 更新現有段落
341
+                $paragraph = $record->paragraphs()->find($paragraphData['id']);
342
+                if ($paragraph) {
343
+                    $paragraph->update([
344
+                        'type' => $paragraphData['type'],
345
+                        'order' => $paragraphData['order'],
346
+                        'content' => $paragraphData['content'],
347
+                    ]);
348
+                    \Log::info("更新段落 ID: {$paragraphData['id']}");
349
+                }
350
+            } else {
351
+                // 建立新段落
352
+                $newParagraph = $record->paragraphs()->create([
353
+                    'type' => $paragraphData['type'],
354
+                    'order' => $paragraphData['order'],
355
+                    'content' => $paragraphData['content'],
356
+                ]);
357
+                \Log::info("建立新段落 ID: {$newParagraph->id}");
358
+            }
359
+        }
360
+
361
+        \Log::info("=== Paragraphs 關聯更新完成 ===");
362
+    }
363
+
364
+    /**
365
+     * 建立 paragraphs 關聯(新增時使用)
366
+     */
367
+    protected function createParagraphs(Model $record, array $paragraphs): void
368
+    {
369
+        \Log::info("=== 開始建立 Paragraphs 關聯 ===");
370
+
371
+        foreach ($paragraphs as $paragraphData) {
372
+            $newParagraph = $record->paragraphs()->create([
373
+                'type' => $paragraphData['type'],
374
+                'order' => $paragraphData['order'],
375
+                'content' => $paragraphData['content'],
376
+            ]);
377
+            \Log::info("建立新段落 ID: {$newParagraph->id}");
378
+        }
379
+
380
+        \Log::info("=== Paragraphs 關聯建立完成 ===");
381
+    }
382
+
383
+    /**
384
+     * 清理 HTML 內容
385
+     */
386
+    protected function cleanHtml(string $html): string
387
+    {
388
+        // 這裡可以根據需求自定義清理邏輯
389
+        // 例如:移除危險標籤、清理空白等
390
+        return trim($html);
391
+    }
392
+
393
+    /**
394
+     * 後處理 paragraphs(儲存後使用)
395
+     */
396
+    protected function postProcessParagraphs($paragraphs): void
397
+    {
398
+        foreach ($paragraphs as $paragraph) {
399
+            \Log::info("後處理段落:", [
400
+                'id' => $paragraph->id,
401
+                'type' => $paragraph->type,
402
+                'content_keys' => array_keys($paragraph->content ?? [])
403
+            ]);
404
+
405
+            // 這裡可以進行:
406
+            // - 快取更新
407
+            // - 搜尋索引更新
408
+            // - 通知發送
409
+            // 等後續處理
410
+        }
411
+    }
412
+
413
+    /**
414
+     * 獲取並格式化關聯資料(載入時的輔助方法)
415
+     */
416
+    protected function loadAndFormatParagraphs(Model $record): array
417
+    {
418
+        $paragraphs = $record->paragraphs()
419
+            ->orderBy('order')
420
+            ->get()
421
+            ->toArray();
422
+
423
+        return $this->formatParagraphsForForm($paragraphs);
424
+    }
425
+
426
+    /**
427
+     * 處理表單狀態中的 paragraphs(儲存時的輔助方法)
428
+     */
429
+    protected function processFormParagraphs(): ?array
430
+    {
431
+        $formState = $this->form->getState();
432
+
433
+        if (!isset($formState['paragraphs'])) {
434
+            return null;
435
+        }
436
+
437
+        return $this->preprocessParagraphs($formState['paragraphs']);
438
+    }
439
+}

+ 34
- 0
database/migrations/2025_06_18_072530_create_esgs_table.php View File

@@ -0,0 +1,34 @@
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('esgs', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->string('keyword')->index();
17
+            $table->json('title')->comment('標題');
18
+            $table->json("description")->comment("短文");
19
+            $table->string("banner_pc")->comment("banner for pc");
20
+            $table->string("banner_mobile")->comment("banner for mobile");
21
+            $table->json("banner_alt")->comment("banner alt");
22
+            $table->timestamps();
23
+            $table->softDeletes();
24
+        });
25
+    }
26
+
27
+    /**
28
+     * Reverse the migrations.
29
+     */
30
+    public function down(): void
31
+    {
32
+        Schema::dropIfExists('esgs');
33
+    }
34
+};

+ 32
- 0
database/migrations/2025_06_18_075604_create_esg_paragraphs_table.php View File

@@ -0,0 +1,32 @@
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('esg_paragraphs', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('esg_id')->constrained()->onDelete('cascade');
17
+            $table->tinyInteger("type")->comment("1: 純editor 2:表格 3:影片 or 圖片 4:左右圖文");
18
+            $table->json("content");
19
+            $table->integer("order")->comment("排序");
20
+            $table->timestamps();
21
+            $table->index(['esg_id', 'order']);
22
+        });
23
+    }
24
+
25
+    /**
26
+     * Reverse the migrations.
27
+     */
28
+    public function down(): void
29
+    {
30
+        Schema::dropIfExists('esg_paragraphs');
31
+    }
32
+};

+ 35
- 0
database/migrations/2025_07_02_041600_create_esg_mains_table.php View File

@@ -0,0 +1,35 @@
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('esg_mains', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->json('title')->comment("標題");
17
+            $table->json('description')->comment("短文");
18
+            $table->string('og_img')->nullable()->comment('seo image');
19
+            $table->json("meta_title")->nullable()->comment("seo title");
20
+            $table->json("meta_keyword")->nullable()->comment("seo keyword");
21
+            $table->json("meta_description")->nullable()->comment("seo description");
22
+            $table->string('img_pc')->nullable()->comment('Image PC');
23
+            $table->string('img_mobile')->nullable()->comment('Image MOBILE');
24
+            $table->timestamps();
25
+        });
26
+    }
27
+
28
+    /**
29
+     * Reverse the migrations.
30
+     */
31
+    public function down(): void
32
+    {
33
+        Schema::dropIfExists('esg_mains');
34
+    }
35
+};

+ 17
- 0
resources/views/filament/pages/esg-main.blade.php View File

@@ -0,0 +1,17 @@
1
+<x-filament-panels::page>
2
+    {{-- ✅ 確保不直接輸出陣列 --}}
3
+    <form wire:submit="save">
4
+        {{ $this->form }}
5
+
6
+        <div class="mt-6">
7
+            <x-filament::button
8
+                type="submit"
9
+                color="primary"
10
+                wire:loading.attr="disabled"
11
+            >
12
+                <span wire:loading.remove>儲存設定</span>
13
+                <span wire:loading>儲存中...</span>
14
+            </x-filament::button>
15
+        </div>
16
+    </form>
17
+</x-filament-panels::page>

+ 5
- 0
routes/api.php View File

@@ -1,5 +1,6 @@
1 1
 <?php
2 2
 
3
+use App\Http\Controllers\Api\EsgController;
3 4
 use App\Http\Controllers\Api\NewsController;
4 5
 use Illuminate\Http\Request;
5 6
 use Illuminate\Support\Facades\Route;
@@ -24,5 +25,9 @@ Route::prefix('{locale}')->group(function (){
24 25
         Route::get('/list', [NewsController::class, 'list']);
25 26
         Route::get('/detail/{id}', [NewsController::class, 'detail'])->whereIn('locale', ["tw", "en", "jp"])->where('id', '[0-9]+');
26 27
     });
28
+    Route::prefix('esg')->group(function () {
29
+        Route::get('/', [EsgController::class, "main"]);
30
+        Route::get('/{key}', [EsgController::class, "esgContent"]);
31
+    });
27 32
 })->whereIn('locale', ["tw", "en"]);
28 33