ソースを参照

back update with api

コミット
ba314efa9f
共有41 個のファイルを変更した1546 個の追加293 個の削除を含む
  1. 25
    2
      app/Filament/Resources/BadgeResource.php
  2. 17
    9
      app/Filament/Resources/BannerResource.php
  3. 26
    6
      app/Filament/Resources/HistoryResource.php
  4. 14
    5
      app/Filament/Resources/NewsCategoryResource.php
  5. 68
    66
      app/Filament/Resources/NewsResource.php
  6. 18
    4
      app/Filament/Resources/ProfilePartResource.php
  7. 106
    67
      app/Filament/Resources/ProjectResource.php
  8. 9
    1
      app/Filament/Resources/RegionResource.php
  9. 9
    1
      app/Filament/Resources/TagResource.php
  10. 42
    0
      app/Http/Controllers/Api/DeeplTranslateController.php
  11. 39
    0
      app/Http/Controllers/Api/HistoryController.php
  12. 44
    0
      app/Http/Controllers/Api/HomePageController.php
  13. 116
    0
      app/Http/Controllers/Api/NewsController.php
  14. 39
    0
      app/Http/Controllers/Api/ProfilePageController.php
  15. 126
    0
      app/Http/Controllers/Api/ProjectController.php
  16. 41
    0
      app/Http/Helper/Helper.php
  17. 2
    2
      app/Models/Badge.php
  18. 17
    0
      app/Models/DeeplGlossary.php
  19. 2
    2
      app/Models/News.php
  20. 10
    3
      app/Models/NewsParagraph.php
  21. 0
    28
      app/Models/NewsParagraphPhoto.php
  22. 70
    5
      app/Models/Project.php
  23. 0
    15
      app/Models/ProjectSummary.php
  24. 4
    1
      app/Providers/Filament/AdminPanelProvider.php
  25. 96
    0
      app/Service/DeepLService.php
  26. 44
    0
      app/Supports/Response.php
  27. 1
    0
      bootstrap/app.php
  28. 5
    2
      composer.json
  29. 339
    1
      composer.lock
  30. 84
    0
      config/sanctum.php
  31. 20
    0
      config/thirdPartyApi.php
  32. 29
    0
      database/migrations/2024_09_25_043733_create_deepl_glossary_table.php
  33. 1
    0
      database/migrations/2025_09_04_073524_create_news_table.php
  34. 2
    0
      database/migrations/2025_09_17_033657_create_news_paragraphs_table.php
  35. 1
    1
      database/migrations/2025_09_17_033900_create_badges_table.php
  36. 0
    34
      database/migrations/2025_09_18_033227_create_news_paragraph_photos_table.php
  37. 6
    1
      database/migrations/2025_09_18_091406_create_projects_table.php
  38. 3
    0
      database/migrations/2025_09_18_092245_create_regions_table.php
  39. 0
    37
      database/migrations/2025_09_18_092430_create_project_summaries_table.php
  40. 33
    0
      database/migrations/2025_10_14_030744_create_personal_access_tokens_table.php
  41. 38
    0
      routes/api.php

+ 25
- 2
app/Filament/Resources/BadgeResource.php ファイルの表示

@@ -4,13 +4,16 @@ namespace App\Filament\Resources;
4 4
 
5 5
 use AbdulmajeedJamaan\FilamentTranslatableTabs\TranslatableTabs;
6 6
 use App\Filament\Resources\BadgeResource\Pages;
7
+use App\Service\DeepLService;
7 8
 use App\Filament\Resources\BadgeResource\RelationManagers;
8 9
 use App\Models\Badge;
9 10
 use Filament\Forms;
11
+use Filament\Forms\Components\DatePicker;
10 12
 use Filament\Forms\Components\FileUpload;
11 13
 use Filament\Forms\Components\Group;
12 14
 use Filament\Forms\Components\Radio;
13 15
 use Filament\Forms\Components\Section;
16
+use Filament\Forms\Components\Select;
14 17
 use Filament\Forms\Components\TextInput;
15 18
 use Filament\Forms\Form;
16 19
 use Filament\Resources\Resource;
@@ -21,6 +24,7 @@ use Filament\Tables\Columns\TextInputColumn;
21 24
 use Filament\Tables\Table;
22 25
 use Illuminate\Database\Eloquent\Builder;
23 26
 use Illuminate\Database\Eloquent\SoftDeletingScope;
27
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
24 28
 
25 29
 class BadgeResource extends Resource
26 30
 {
@@ -45,11 +49,30 @@ class BadgeResource extends Resource
45 49
             ->schema([
46 50
                 Section::make("")->schema([
47 51
                     Group::make()->schema([
48
-                        TextInput::make("title")->label("徽章標題")->translatableTabs()->columnSpanFull(),
52
+                        Group::make()->schema([
53
+                            Translate::make()->schema(fn (string $locale) => [
54
+                                TextInput::make('title')->required($locale == 'zh_TW')->maxLength(40)->label("徽章標題")
55
+                            ])
56
+                            ->locales(["zh_TW", "en"])
57
+                            ->actions([
58
+                                app(DeepLService::class)->createTranslationAction("Main", ["title"])
59
+                            ])->columnSpan(1),
60
+                        ])->columns(2)->columnSpanFull(),
61
+                        Group::make()->schema([
62
+                            Select::make("reward_year")->label("認證年份")->options(function () {
63
+                                $currentYear = now()->year;
64
+                                $years = [];
65
+                                for ($i = $currentYear - 20; $i <= $currentYear +5; $i++) {
66
+                                    $years[$i] = strval($i) . '年';
67
+                                }
68
+                                return $years;
69
+                            })->columnSpan(1),
70
+                        ])->columns(2)->columnSpanFull(),
49 71
                         Group::make()->schema([
50 72
                             FileUpload::make("img_url")->label("圖片")->directory("badge")->columnSpan(1),
51
-                            TextInput::make("img_alt")->label("圖片註釋")->translatableTabs()->columnSpan(1)
52 73
                         ])->columnSpanFull(),
74
+                    ])->columns(4)->columnSpanFull(),
75
+                    Group::make()->schema([
53 76
                         Radio::make("visible")->label("顯示/不顯示")->options([1 => "顯示", 0 => "不顯示"])->inline()->default(1)
54 77
                         ->columnSpan(2)
55 78
                     ])->columns(4)->columnSpanFull(),

+ 17
- 9
app/Filament/Resources/BannerResource.php ファイルの表示

@@ -4,6 +4,7 @@ namespace App\Filament\Resources;
4 4
 
5 5
 use AbdulmajeedJamaan\FilamentTranslatableTabs\TranslatableTabs;
6 6
 use App\Filament\Resources\BannerResource\Pages;
7
+use App\Service\DeepLService;
7 8
 use App\Filament\Resources\BannerResource\RelationManagers;
8 9
 use App\Models\Banner;
9 10
 use Filament\Forms;
@@ -20,6 +21,7 @@ use Filament\Tables\Columns\ImageColumn;
20 21
 use Filament\Tables\Table;
21 22
 use Illuminate\Database\Eloquent\Builder;
22 23
 use Illuminate\Database\Eloquent\SoftDeletingScope;
24
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
23 25
 
24 26
 class BannerResource extends Resource
25 27
 {
@@ -46,17 +48,23 @@ class BannerResource extends Resource
46 48
                 //
47 49
                 Section::make("")->schema([
48 50
                     Group::make()->schema([
49
-                        TranslatableTabs::make('')
50
-                        ->schema([
51
-                            Textarea::make("title")->label("大字標題"),
52
-                            Textarea::make("content")->label("小字標題")
51
+                        Translate::make()->schema(fn (string $locale) => [
52
+                            TextInput::make('title')->required($locale == 'zh_TW')->label("大字標題"),
53
+                            TextInput::make('content')->required($locale == 'zh_TW')->label("小字標題")
54
+                        ])
55
+                        ->locales(["zh_TW", "en"])
56
+                        ->actions([
57
+                            app(DeepLService::class)->createTranslationAction("Main", ["title", "content"])
53 58
                         ])->columnSpanFull(),
54 59
                         Group::make()->schema([
55
-                            FileUpload::make("img_url")->label("圖片")
56
-                            ->directory("banners")
57
-                            ->columnSpan(1),
58
-                            TextInput::make("img_alt")->label("圖片註釋")
59
-                            ->translatableTabs()->columnSpan(1)
60
+                            FileUpload::make("img_url")->label("圖片")->directory("banners")->columnSpan(1),
61
+                            Translate::make()->schema(fn (string $locale) => [
62
+                                TextInput::make('img_alt')->required($locale == 'zh_TW')->label("圖片註釋")
63
+                            ])
64
+                            ->locales(["zh_TW", "en"])
65
+                            ->actions([
66
+                                app(DeepLService::class)->createTranslationAction("Main", ["img_alt"])
67
+                            ])->columnSpan(1),
60 68
                         ])->columnSpanFull(),
61 69
                         Radio::make("visible")->label("顯示/不顯示")->options([1 => "顯示", 0 => "不顯示"])->inline()->default(1)->columnSpan(2)
62 70
                     ])->columns(4)->columnSpanFull(),

+ 26
- 6
app/Filament/Resources/HistoryResource.php ファイルの表示

@@ -3,6 +3,7 @@
3 3
 namespace App\Filament\Resources;
4 4
 
5 5
 use App\Filament\Resources\HistoryResource\Pages;
6
+use App\Service\DeepLService;
6 7
 use App\Filament\Resources\HistoryResource\RelationManagers;
7 8
 use App\Models\History;
8 9
 use Filament\Forms;
@@ -21,6 +22,7 @@ use Filament\Tables\Filters\SelectFilter;
21 22
 use Filament\Tables\Table;
22 23
 use Illuminate\Database\Eloquent\Builder;
23 24
 use Illuminate\Database\Eloquent\SoftDeletingScope;
25
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
24 26
 
25 27
 class HistoryResource extends Resource
26 28
 {
@@ -30,6 +32,8 @@ class HistoryResource extends Resource
30 32
     protected static ?string $navigationLabel = "宏匯里程管理";
31 33
     protected static ?string $navigationGroup = '關於宏匯';
32 34
     protected static ?string $modelLabel = "宏匯里程管理";
35
+    public ?string $tableSortColumn = null;
36
+    public ?string $tableSortDirection = null;
33 37
 
34 38
     public static function form(Form $form): Form
35 39
     {
@@ -53,11 +57,23 @@ class HistoryResource extends Resource
53 57
                         }
54 58
                         return $months;
55 59
                     })->default(now()->month)->columnSpan(1),
56
-                    TextInput::make("title")->label("標題")->translatableTabs()->columnSpanFull(),
60
+                    Translate::make()->schema(fn (string $locale) => [
61
+                        TextInput::make('title')->required($locale == 'zh_TW')->label("標題"),
62
+                    ])
63
+                    ->locales(["zh_TW", "en"])
64
+                    ->actions([
65
+                        app(DeepLService::class)->createTranslationAction("Main", ["title"])
66
+                    ])->columnSpanFull(),
57 67
                     Group::make()->schema([
58 68
                         FileUpload::make("img_url")->label("圖片")->directory("histories")
59 69
                         ->columnSpanFull(),
60
-                        TextInput::make("img_alt")->label("圖片註釋")->translatableTabs()->columnSpanFull()
70
+                        Translate::make()->schema(fn (string $locale) => [
71
+                            TextInput::make('img_alt')->label("圖片註釋"),
72
+                        ])
73
+                        ->locales(["zh_TW", "en"])
74
+                        ->actions([
75
+                            app(DeepLService::class)->createTranslationAction("Image", ["img_alt"])
76
+                        ])->columnSpanFull()
61 77
                     ])->columnSpanFull(),
62 78
                     Radio::make("visible")->label("顯示/不顯示")->options([1 => "顯示", 0 => "不顯示"])->inline()->default(1)->columnSpan(2)
63 79
                 ])->columns(3)
@@ -71,9 +87,7 @@ class HistoryResource extends Resource
71 87
                 TextColumn::make("selected_year")->label("年份")->alignCenter(),
72 88
                 TextColumn::make("selected_month")->label("月份")->alignCenter(),
73 89
                 TextColumn::make("title")->label("標題"),
74
-                ImageColumn::make("img_url")->alignCenter(),
75
-                TextColumn::make('created_at')->label("建立時間")->date(),
76
-                TextColumn::make('updated_at')->label("更新時間")->date(),
90
+                ImageColumn::make("img_url")->label("圖片")->disk(config("filesystem.default"))->alignCenter(),
77 91
             ])
78 92
             ->filters([
79 93
                 SelectFilter::make('selected_year')->label("年份")
@@ -105,7 +119,13 @@ class HistoryResource extends Resource
105 119
                 Tables\Actions\BulkActionGroup::make([
106 120
                     Tables\Actions\DeleteBulkAction::make(),
107 121
                 ]),
108
-            ]);
122
+            ])
123
+            ->modifyQueryUsing(function (Builder $query) {
124
+                // ✅ 正確:多欄位排序
125
+                return $query
126
+                    ->orderBy('selected_year', 'desc')
127
+                    ->orderBy('selected_month', 'desc');
128
+            });
109 129
     }
110 130
 
111 131
     public static function getRelations(): array

+ 14
- 5
app/Filament/Resources/NewsCategoryResource.php ファイルの表示

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
5 5
 use App\Filament\Resources\NewsCategoryResource\Pages;
6 6
 use App\Filament\Resources\NewsCategoryResource\RelationManagers;
7 7
 use App\Models\NewsCategory;
8
+use App\Service\DeepLService;
8 9
 use Filament\Forms;
9 10
 use Filament\Forms\Components\Group;
10 11
 use Filament\Forms\Components\Radio;
@@ -17,6 +18,7 @@ use Filament\Tables\Columns\TextColumn;
17 18
 use Filament\Tables\Table;
18 19
 use Illuminate\Database\Eloquent\Builder;
19 20
 use Illuminate\Database\Eloquent\SoftDeletingScope;
21
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
20 22
 
21 23
 class NewsCategoryResource extends Resource
22 24
 {
@@ -34,11 +36,18 @@ class NewsCategoryResource extends Resource
34 36
             ->schema([
35 37
                 Section::make("")->schema([
36 38
                     Group::make()->schema([
37
-                        TextInput::make("name")->label("標題")->translatableTabs()->columnSpanFull(),
38
-                        Radio::make("visible")->label("顯示/不顯示")->options([1 => "顯示", 0 => "不顯示"])->inline()->default(1)
39
-                        ->columnSpan(2)
40
-                    ])->columns(4)->columnSpanFull()
41
-                ])
39
+                        Translate::make()->schema(fn (string $locale) => [
40
+                            TextInput::make('name')->required($locale == 'zh_TW')->maxLength(40)->label("分類名稱")->columnSpan(1)
41
+                        ])
42
+                        ->locales(["zh_TW", "en"])
43
+                        ->actions([
44
+                            app(DeepLService::class)->createTranslationAction("Main", ["name"])
45
+                        ])->columnSpan(2),
46
+                    ])->columns(4)->columnSpanFull(),
47
+                    Group::make()->schema([
48
+                        TextInput::make('order')->label("排序")->integer()->default(0)->columnSpan(1)
49
+                    ])->columnSpanFull()->columns(4),
50
+                ])->columns(4)
42 51
             ]);
43 52
     }
44 53
 

+ 68
- 66
app/Filament/Resources/NewsResource.php ファイルの表示

@@ -13,6 +13,7 @@ use Filament\Forms\Components\FileUpload;
13 13
 use Filament\Forms\Components\Group;
14 14
 use Filament\Forms\Components\Radio;
15 15
 use Filament\Forms\Components\Repeater;
16
+use Filament\Forms\Components\RichEditor;
16 17
 use Filament\Forms\Components\Section;
17 18
 use Filament\Forms\Components\Select;
18 19
 use Filament\Forms\Components\Tabs;
@@ -33,6 +34,8 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
33 34
 use Illuminate\Support\Str;
34 35
 use Malzariey\FilamentLexicalEditor\Enums\ToolbarItem;
35 36
 use Malzariey\FilamentLexicalEditor\FilamentLexicalEditor;
37
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
38
+use App\Service\DeepLService;
36 39
 
37 40
 class NewsResource extends Resource
38 41
 {
@@ -68,32 +71,43 @@ class NewsResource extends Resource
68 71
                         ->native(false)
69 72
                         ->closeOnDateSelection()
70 73
                         ->columnSpan(1),
74
+                        Translate::make()->schema(fn (string $locale) => [
75
+                            TextInput::make("title")->required($locale == 'zh_TW')->label("標題")->columnSpan(1),
76
+                            TextInput::make("written_by")->required($locale == 'zh_TW')->label("撰文者")->columnSpan(1),
77
+                            Textarea::make("description")->required($locale == 'zh_TW')->label("簡述")->columnSpan(2)
78
+                        ])
79
+                        ->locales(["zh_TW", "en"])
80
+                        ->actions([
81
+                            app(DeepLService::class)->createTranslationAction("Main", ["title", "written_by", "description"])
82
+                        ])->columns(2)
83
+                        ->columnSpan(2)
84
+                        ->id("Main-content"),
71 85
                         FileUpload::make("news_img")->label("圖片")
72
-                        ->directory("news")->maxSize(122880)
86
+                        ->directory("news")
73 87
                         ->columnSpan(2),
74
-                        TranslatableTabs::make('')
75
-                        ->schema([
76
-                            TextInput::make("title")
77
-                            ->label("標題")
78
-                            ->columnSpan(1),
79
-                            TextInput::make("written_by")
80
-                            ->label("撰文者")
81
-                            ->columnSpan(1),
82
-                            Textarea::make("description")
83
-                            ->label("簡述")
84
-                            ->columnSpan(2)
85
-                        ])->columns(2)->columnSpan(2),
88
+                        Translate::make()->schema(fn (string $locale) => [
89
+                            TextInput::make('news_img_alt')->label("圖片註釋")
90
+                        ])
91
+                        ->locales(["zh_TW", "en"])
92
+                        ->actions([
93
+                            app(DeepLService::class)->createTranslationAction("NewsImageAlt", ["news_img_alt"])
94
+                        ])->columnSpan(2)
95
+                        ->id("Main-alt"),
86 96
                     ])->columns(2),
87 97
                     Tab::make("SEO")->schema([
88
-                        TranslatableTabs::make('')
89
-                        ->schema([
98
+                        Translate::make()->schema(fn (string $locale) => [
90 99
                             TextInput::make("meta_title")->label("SEO 標題"),
91 100
                             TextInput::make("meta_keyword")->label("SEO 關鍵字"),
92 101
                             Textarea::make("meta_description")->label("SEO 簡述")
102
+                        ])
103
+                        ->locales(["zh_TW", "en"])
104
+                        ->actions([
105
+                            app(DeepLService::class)->createTranslationAction("Seo", ["meta_title", "meta_keyword", "meta_description"])
93 106
                         ])->columnSpanFull(),
94 107
                         FileUpload::make('meta_img')->label("放大預覽圖")
95 108
                         ->directory("news/seo")
96
-                        ->columnSpan(1),
109
+                        ->columnSpan(1)
110
+                        ->id("Seo"),
97 111
                     ]),
98 112
                     Tab::make("內文編輯")->schema([
99 113
                         Repeater::make("paragraphs")->schema([
@@ -112,62 +126,50 @@ class NewsResource extends Resource
112 126
                             ])->label("")->inline()->default(1)->Live(),
113 127
                             Group::make()->schema([
114 128
                                 Section::make('')->schema([
115
-                                    Repeater::make("multiple_images")->schema([
116
-                                        TextInput::make('para_img_item_key')
117
-                                            ->default(fn () => Str::random())
118
-                                            ->hidden()
119
-                                            ->afterStateHydrated(function (TextInput $component, $state) {
120
-                                                if (empty($state)) {
121
-                                                    $component->state(Str::random());
122
-                                                }
123
-                                            }),
124
-                                        TextInput::make("img_alt")->label("圖片註釋")->translatableTabs()->columnSpan(1),
125
-                                        FileUpload::make('img_url')->label("")
126
-                                        ->directory("news/paragraphs")
127
-                                        ->maxFiles(10),
129
+                                    FileUpload::make('img_url')->label("")->directory("news/paragraphs"),
130
+                                    Translate::make()->schema(fn (string $locale) => [
131
+                                        TextInput::make('img_alt')->label("圖片註釋")
128 132
                                     ])
129
-                                    ->relationship("photos")
130
-                                    ->label("")
131
-                                    ->orderColumn('order')
133
+                                    ->locales(["zh_TW", "en"])
134
+                                    ->actions([
135
+                                        app(DeepLService::class)->createTranslationAction("ParagraphImgAlt", ["img_alt"])
136
+                                    ])->columnSpan(1)
137
+                                    ->id(fn ($get) => "para_img_" . $get('item_key')),
132 138
                                 ])
133 139
                             ])->visible(fn (Get $get):bool => $get("paragraph_type") == 1),
134 140
                             Group::make()->schema([
135
-                                FilamentLexicalEditor::make('text_content_tw')
136
-                                    ->label('繁體中文內容')
137
-                                    ->id(fn ($get) => "text_content_tw_" . $get('item_key') . "_" . uniqid())
138
-                                    ->enabledToolbars($editor_toolbar)
139
-                                    ->columnSpanFull(),
140
-                                FilamentLexicalEditor::make('text_content_en')
141
-                                    ->label('English Content')
142
-                                    ->id(fn ($get) => "text_content_en_" . $get('item_key') . "_" . uniqid())
143
-                                    ->enabledToolbars($editor_toolbar)
144
-                                    ->columnSpanFull(),
141
+                                Translate::make()->schema(fn (string $locale) => [
142
+                                    RichEditor::make('text_content')
143
+                                        ->toolbarButtons([
144
+                                            'blockquote',
145
+                                            'bold',
146
+                                            'bulletList',
147
+                                            'h2',
148
+                                            'h3',
149
+                                            'italic',
150
+                                            'link',
151
+                                            'orderedList',
152
+                                            'redo',
153
+                                            'strike',
154
+                                            'underline',
155
+                                            'undo',
156
+                                        ])
157
+                                        ->disableToolbarButtons([
158
+                                            'blockquote',
159
+                                            'strike',
160
+                                            'attachFiles',
161
+                                        ])
162
+                                        ->fileAttachmentsDisk('s3')
163
+                                        ->fileAttachmentsDirectory('attachments')
164
+                                        ->fileAttachmentsVisibility('private'),
165
+                                        ])
166
+                                ->locales(["zh_TW", "en"])
167
+                                ->actions([
168
+                                    app(DeepLService::class)->createTranslationAction("ParagraphText", ["text_content"])
169
+                                ])->columnSpan(1)
170
+                                ->id(fn ($get) => "para_text_" . $get('item_key')),
145 171
                             ])->visible(fn (Get $get):bool => $get("paragraph_type") == 2),
146 172
                         ])
147
-                        ->mutateRelationshipDataBeforeFillUsing(function (array $data): array {
148
-
149
-                            if ($data['paragraph_type'] == 2 && !empty($data['text_content'])) {
150
-                                $content = is_string($data['text_content'])
151
-                                    ? json_decode($data['text_content'], true)
152
-                                    : $data['text_content'];
153
-
154
-                                if (is_array($content)) {
155
-                                    $data['text_content_tw'] = $content['zh_TW'] ?? '';
156
-                                    $data['text_content_en'] = $content['en'] ?? '';
157
-                                }
158
-                            }
159
-
160
-                            return $data;
161
-                        })
162
-                        ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array{
163
-                            if ($data['paragraph_type'] == 2) {
164
-                                $data["text_content"] = ["zh_TW" => $data["text_content_tw"], "en" => $data["text_content_en"]];
165
-                                // 移除臨時的分離欄位,只保留合併的 JSON 欄位
166
-                                unset($data['text_content_tw']);
167
-                                unset($data['text_content_en']);
168
-                            }
169
-                            return $data;
170
-                        })
171 173
                         ->relationship("paragraphs")
172 174
                         ->label("段落")
173 175
                         ->collapsible()

+ 18
- 4
app/Filament/Resources/ProfilePartResource.php ファイルの表示

@@ -19,6 +19,8 @@ use Filament\Tables\Columns\ImageColumn;
19 19
 use Filament\Tables\Columns\TextColumn;
20 20
 use Filament\Tables\Table;
21 21
 use Illuminate\Database\Eloquent\Builder;
22
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
23
+use App\Service\DeepLService;
22 24
 
23 25
 class ProfilePartResource extends Resource
24 26
 {
@@ -43,16 +45,28 @@ class ProfilePartResource extends Resource
43 45
         return $form
44 46
             ->schema([
45 47
                 Section::make("")->schema([
46
-                    TranslatableTabs::make('')
47
-                    ->schema([
48
+                    Translate::make()->schema(fn (string $locale) => [
48 49
                         TextInput::make("title")->label("標題")->columnSpanFull(),
49 50
                         Textarea::make("content")->label("內文")->columnSpanFull()
50
-                    ])->columnSpanFull(),
51
+                    ])
52
+                    ->locales(["zh_TW", "en"])
53
+                    ->actions([
54
+                        app(DeepLService::class)->createTranslationAction("Main", ["title", "content"])
55
+                    ])
56
+                    ->id("Main")->columnSpanFull(),
51 57
                     Group::make()->schema([
52 58
                         FileUpload::make("img_url")->label("圖片")
53 59
                         ->directory("profile-parts")
54 60
                         ->columnSpanFull(),
55
-                        TextInput::make("img_alt")->label("圖片註釋")->translatableTabs()->columnSpanFull()
61
+                        Translate::make()->schema(fn (string $locale) => [
62
+                            TextInput::make("img_alt")->label("圖片註釋")->columnSpanFull()
63
+                        ])
64
+                        ->locales(["zh_TW", "en"])
65
+                        ->actions([
66
+                            app(DeepLService::class)->createTranslationAction("Img", ["img_alt"])
67
+                        ])
68
+                        ->id("Img")
69
+                        ->columnSpanFull(),
56 70
                     ])->columnSpanFull(),
57 71
                 ])->columns(3)
58 72
             ]);

+ 106
- 67
app/Filament/Resources/ProjectResource.php ファイルの表示

@@ -22,102 +22,137 @@ use Filament\Forms\Form;
22 22
 use Filament\Forms\Get;
23 23
 use Filament\Resources\Resource;
24 24
 use Filament\Tables;
25
+use Filament\Tables\Columns\ImageColumn;
26
+use Filament\Tables\Columns\TextColumn;
25 27
 use Filament\Tables\Table;
26 28
 use Illuminate\Database\Eloquent\Builder;
27 29
 use Illuminate\Database\Eloquent\SoftDeletingScope;
28 30
 use Illuminate\Support\Facades\Storage;
31
+use Illuminate\Support\Str;
32
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
33
+use App\Service\DeepLService;
29 34
 
30 35
 class ProjectResource extends Resource
31 36
 {
32 37
     protected static ?string $model = Project::class;
33 38
 
34 39
     protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
40
+    protected static ?string $navigationGroup = '專案項目管理';
35 41
     protected static ?string $navigationLabel = "專案項目管理";
36 42
     protected static ?string $modelLabel = "專案項目管理";
43
+    protected static ?int $navigationSort = 5;
37 44
 
38 45
     public static function form(Form $form): Form
39 46
     {
40 47
         return $form
41 48
             ->schema([
42 49
                 Section::make("")->schema([
43
-                    Select::make("region_id")->label("地區")->options(function (){
44
-                        return Region::where("visible",true)->pluck("name", "id");
45
-                    }),
50
+                    Translate::make()->schema(fn (string $locale) => [
51
+                        TextInput::make("name")->label("項目名稱"),
52
+                        TextInput::make("sub_name")->label("項目子名稱"),
53
+                    ])
54
+                    ->locales(["zh_TW", "en"])
55
+                    ->actions([
56
+                        app(DeepLService::class)->createTranslationAction("Main", ["name", "sub_name"])
57
+                    ])->columnSpanFull()->id("main"),
46 58
                     Select::make('tags')
47 59
                         ->multiple()
48 60
                         ->relationship('tags', 'name')
49 61
                         ->preload()
50 62
                         ->label('標籤'),
51
-                    TextInput::make("name")->label("項目名稱")->translatableTabs(),
52
-                    FileUpload::make("img_url")->label("圖片")->directory("project"),
53
-                    TextInput::make("img_alt")->label("圖片註釋")->translatableTabs(),
54
-                    Tabs::make()->tabs([
55
-                        Tab::make("專案概要")->schema([
56
-                            TranslatableTabs::make('')
57
-                                ->schema([
58
-                                    TextInput::make("address")->label("地址"),
59
-                                    Textarea::make("floor_plan")->label("樓層規劃"),
60
-                                    Textarea::make("building_structure")->label("建築結構"),
61
-                                    Textarea::make("design_unit")->label("設計團隊")
62
-                                ])
63
-                                ->columnSpanFull(),
64
-                            Select::make('badges')
65
-                                ->multiple()
66
-                                ->relationship('badges', 'title')
67
-                                ->getOptionLabelFromRecordUsing(function ($record) {
68
-                                    $imageHtml = $record->img_url
69
-                                        ? '<img src="' . Storage::url($record->img_url) . '" class="w-6 h-6 rounded-full mr-2 inline-block" />'
70
-                                        : '<div class="w-6 h-6 bg-gray-200 rounded-full mr-2 inline-block"></div>';
63
+                    FileUpload::make("img_url")->label("圖片")->directory("project")->multiple()->maxFiles(5),
64
+                ]),
65
+                Tabs::make()->schema([
66
+                    Tab::make("專案概要")->schema([
67
+                        Select::make("region_id")->label("地區")->options(function (){
68
+                            return Region::where("visible",true)->pluck("name", "id");
69
+                        }),
70
+                        Translate::make()->schema(fn (string $locale) => [
71
+                            Textarea::make("summaries")->label("簡述"),
72
+                            TextInput::make("address")->label("地址"),
73
+                            Textarea::make("floor_plan")->label("樓層規劃"),
74
+                            Textarea::make("building_structure")->label("建築結構"),
75
+                            Textarea::make("design_unit")->label("設計團隊")
76
+                        ])
77
+                        ->locales(["zh_TW", "en"])
78
+                        ->actions([
79
+                            app(DeepLService::class)->createTranslationAction("summaries", ["summaries","address",
80
+                            "floor_plan","building_structure","design_unit"])
81
+                        ])->columnSpanFull()->id("summaries"),
82
+                        Select::make('badges')
83
+                            ->multiple()
84
+                            ->relationship('badges', 'title')
85
+                            ->getOptionLabelFromRecordUsing(function ($record) {
86
+                                $imageHtml = $record->img_url
87
+                                    ? '<img src="' . Storage::url($record->img_url) . '" class="w-6 h-6 rounded-full mr-2 inline-block" />'
88
+                                    : '<div class="w-6 h-6 bg-gray-200 rounded-full mr-2 inline-block"></div>';
71 89
 
72
-                                    return new \Illuminate\Support\HtmlString($imageHtml . $record->title);
73
-                                })
74
-                                ->allowHtml()
75
-                                ->preload()
76
-                                ->label('取得標章')
77
-                                ->maxItems(8)
78
-                                ->required(),
79
-                            Repeater::make("summaries")->label("")->schema([
80
-                                TranslatableTabs::make('')
81
-                                ->schema([
82
-                                    TextInput::make("title")->label("標題")->columnSpanFull(),
83
-                                    Textarea::make("content")->label("內文")->columnSpanFull()
84
-                                ])->columnSpanFull(),
90
+                                return new \Illuminate\Support\HtmlString($imageHtml . $record->title);
91
+                            })
92
+                            ->allowHtml()
93
+                            ->preload()
94
+                            ->label('取得標章')
95
+                            ->maxItems(8)
96
+                            ->required(),
97
+                    ]),
98
+                    Tab::make("開發歷程")->schema([
99
+                        Repeater::make("histories")->label("")->schema([
100
+                            TextInput::make('histories_item_key')
101
+                            ->default(fn () => Str::random())
102
+                            ->hidden()
103
+                            ->afterStateHydrated(function (TextInput $component, $state) {
104
+                                if (empty($state)) {
105
+                                    $component->state(Str::random());
106
+                                }
107
+                            }),
108
+                            DatePicker::make("operate_date")->label("歷程日期")->columnSpan(1),
109
+                            Translate::make()->schema(fn (string $locale) => [
110
+                                TextInput::make("title")->label("標題")->columnSpanFull()
85 111
                             ])
86
-                            ->relationship("summaries")
87
-                            ->label("")
88
-                            ->collapsible()
89
-                            ->reorderableWithButtons()
90
-                            ->orderColumn('order')
91
-                            ->cloneable(),
92
-                        ]),
93
-                        Tab::make("開發歷程")->schema([
94
-                            Repeater::make("histories")->label("")->schema([
95
-                                DatePicker::make("operate_date")->label("歷程日期")->columnSpan(1),
96
-                                TextInput::make("title")->label("標題")->translatableTabs()->columnSpanFull()
112
+                            ->locales(["zh_TW", "en"])
113
+                            ->actions([
114
+                                app(DeepLService::class)->createTranslationAction("histories", ["title"])
115
+                            ])->columnSpanFull()
116
+                            ->id(fn ($get) => "histories_" . $get('histories_item_key')),
117
+                        ])
118
+                        ->relationship("histories", function ($query,Get $get, $livewire) {
119
+                            return $query->orderby("operate_date","desc");
120
+                        })
121
+                        ->collapsible()
122
+                        ->cloneable()
123
+                    ]),
124
+                    Tab::make("空間資訊")->schema([
125
+                        Group::make()->schema([
126
+                            Translate::make()->schema(fn (string $locale) => [
127
+                                TextInput::make("contact_unit")->label("物管單位"),
128
+                                TextInput::make("contact_phone")->label("物管電話"),
129
+                                TextInput::make("inversment_phone")->label("招商電話"),
97 130
                             ])
98
-                            ->relationship("histories", function ($query,Get $get, $livewire) {
99
-                                return $query->orderby("operate_date","desc");
100
-                            })
101
-                            ->collapsible()
102
-                            ->cloneable()
103
-                        ]),
104
-                        Tab::make("空間資訊")->schema([
105
-                            Repeater::make("spaceInfos")->label("")->schema([
106
-                                TranslatableTabs::make('')
107
-                                ->schema([
108
-                                    TextInput::make("title")->label("標題")->columnSpanFull(),
109
-                                    Textarea::make("content")->label("內文")->columnSpanFull()
110
-                                ])->columnSpanFull(),
131
+                            ->locales(["zh_TW", "en"])
132
+                            ->actions([
133
+                                app(DeepLService::class)->createTranslationAction("contact", ["contact_unit", "contact_phone", "inversment_phone"])
134
+                            ])->columnSpanFull()->columns(3)
135
+                            ->id("contact"),
136
+                        ])->columnSpanFull(),
137
+                        Repeater::make("spaceInfos")->label("")->schema([
138
+                            Translate::make()->schema(fn (string $locale) => [
139
+                                TextInput::make("title")->label("標題")->columnSpanFull(),
140
+                                Textarea::make("content")->label("內文")->columnSpanFull()
111 141
                             ])
112
-                            ->relationship("spaceInfos")
113
-                            ->label("")
114
-                            ->collapsible()
115
-                            ->reorderableWithButtons()
116
-                            ->orderColumn('order')
117
-                            ->cloneable(),
142
+                            ->locales(["zh_TW", "en"])
143
+                            ->actions([
144
+                                app(DeepLService::class)->createTranslationAction("spaceInfos", ["title", "content"])
145
+                            ])->columnSpanFull()
146
+                            ->id(fn ($get) => "spaceInfos_" . $get('item_key')),
118 147
                         ])
148
+                        ->relationship("spaceInfos")
149
+                        ->label("")
150
+                        ->collapsible()
151
+                        ->reorderableWithButtons()
152
+                        ->orderColumn('order')
153
+                        ->cloneable(),
119 154
                     ])
120
-                ]),
155
+                ])->columnSpanFull(),
121 156
             ]);
122 157
     }
123 158
 
@@ -126,6 +161,10 @@ class ProjectResource extends Resource
126 161
         return $table
127 162
             ->columns([
128 163
                 //
164
+                TextColumn::make("name")->label("項目名稱")->alignCenter(),
165
+                ImageColumn::make("first_list_img_url")->label("列表圖")->alignCenter(),
166
+                TextColumn::make("region.name")->label("地址")->alignCenter()
167
+                ->formatStateUsing(fn ($record) => $record->region->getTranslation("name", "zh_TW") . ' | ' . $record->getTranslation("address", "zh_TW")),
129 168
             ])
130 169
             ->filters([
131 170
                 //

+ 9
- 1
app/Filament/Resources/RegionResource.php ファイルの表示

@@ -18,6 +18,8 @@ use Filament\Tables\Columns\TextInputColumn;
18 18
 use Filament\Tables\Table;
19 19
 use Illuminate\Database\Eloquent\Builder;
20 20
 use Illuminate\Database\Eloquent\SoftDeletingScope;
21
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
22
+use App\Service\DeepLService;
21 23
 
22 24
 class RegionResource extends Resource
23 25
 {
@@ -34,7 +36,13 @@ class RegionResource extends Resource
34 36
             ->schema([
35 37
                 Section::make("")->schema([
36 38
                     Group::make()->schema([
37
-                        TextInput::make("name")->label("名稱")->translatableTabs()->columnSpanFull(),
39
+                        Translate::make()->schema(fn (string $locale) => [
40
+                            TextInput::make('name')->required($locale == 'zh_TW')->label("名稱"),
41
+                        ])
42
+                        ->locales(["zh_TW", "en"])
43
+                        ->actions([
44
+                            app(DeepLService::class)->createTranslationAction("Main", ["name"])
45
+                        ])->columnSpanFull(),
38 46
                         Radio::make("visible")->label("顯示/不顯示")->options([1 => "顯示", 0 => "不顯示"])->inline()->default(1)
39 47
                         ->columnSpan(2)
40 48
                     ])->columns(4)->columnSpanFull()

+ 9
- 1
app/Filament/Resources/TagResource.php ファイルの表示

@@ -18,6 +18,8 @@ use Filament\Tables\Columns\TextInputColumn;
18 18
 use Filament\Tables\Table;
19 19
 use Illuminate\Database\Eloquent\Builder;
20 20
 use Illuminate\Database\Eloquent\SoftDeletingScope;
21
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
22
+use App\Service\DeepLService;
21 23
 
22 24
 class TagResource extends Resource
23 25
 {
@@ -34,7 +36,13 @@ class TagResource extends Resource
34 36
         ->schema([
35 37
             Section::make("")->schema([
36 38
                 Group::make()->schema([
37
-                    TextInput::make("name")->label("標籤名稱")->translatableTabs()->columnSpanFull(),
39
+                    Translate::make()->schema(fn (string $locale) => [
40
+                        TextInput::make('name')->required($locale == 'zh_TW')->label("標籤名稱"),
41
+                    ])
42
+                    ->locales(["zh_TW", "en"])
43
+                    ->actions([
44
+                        app(DeepLService::class)->createTranslationAction("Main", ["name"])
45
+                    ])->columnSpanFull(),
38 46
                     Radio::make("visible")->label("顯示/不顯示")->options([1 => "顯示", 0 => "不顯示"])->inline()->default(1)
39 47
                     ->columnSpan(2)
40 48
                 ])->columns(4)->columnSpanFull()

+ 42
- 0
app/Http/Controllers/Api/DeeplTranslateController.php ファイルの表示

@@ -0,0 +1,42 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\Development;
7
+use App\Service\DeepLService;
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 DeeplTranslateController extends Controller
18
+{
19
+    public function __construct() {
20
+
21
+    }
22
+    public function insert(Request $request){
23
+        $glossaryName = $request->name;
24
+        $glossaryLocaleFrom = $request->localeFrom;
25
+        $glossaryLocaleTo = $request->localeTo;
26
+        $glossaryItems = $request->localeItems;
27
+        $deeplService = new DeepLService();
28
+        return $deeplService->buildGlossary($glossaryName, $glossaryLocaleFrom, $glossaryLocaleTo, $glossaryItems);
29
+    }
30
+
31
+    public function list()
32
+    {
33
+        $deeplService = new DeepLService();
34
+        return $deeplService->listGlossary();
35
+    }
36
+
37
+    public function delete()
38
+    {
39
+        $deeplService = new DeepLService();
40
+        return $deeplService->deleteGlossary();
41
+    }
42
+}

+ 39
- 0
app/Http/Controllers/Api/HistoryController.php ファイルの表示

@@ -0,0 +1,39 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\History;
7
+use App\Supports\Response;
8
+use Carbon\Carbon;
9
+use Illuminate\Http\Request;
10
+use Illuminate\Support\Facades\DB;
11
+use Illuminate\Support\Facades\Log;
12
+
13
+/**
14
+ * @group Lottery Prize
15
+ */
16
+class HistoryController extends Controller
17
+{
18
+    public function __construct(
19
+    )
20
+    {
21
+    }
22
+
23
+    public function list($locate = 'tw')
24
+    {
25
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
26
+        $data = History::where("visible", 1)->orderByDesc("selected_year", "desc")->orderByDesc("selected_month")->get();
27
+        $yearList = History::select("selected_year", \DB::raw("concat(selected_year, '年') as lable"))->distinct()->orderBy("selected_year", "desc")->pluck('lable', 'selected_year');
28
+        $result = [];
29
+        $result["yearList"] = $yearList;
30
+        foreach($data as $item){
31
+            $result["list"][$item->selected_year][] = [
32
+                "operateMonth " => $item->selected_year . "." . $item->selected_month,
33
+                "title" => $item->getTranslation("title", $locate),
34
+                "imgUrl" => $item->img_url_link
35
+            ];
36
+        }
37
+        return Response::ok($result);
38
+    }
39
+}

+ 44
- 0
app/Http/Controllers/Api/HomePageController.php ファイルの表示

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Http\Requests\RegistEPaperRequest;
7
+use App\Models\EpaperSubscription;
8
+use App\Models\HomepageToTop;
9
+use App\Models\News;
10
+use App\Models\Setting;
11
+use App\Supports\Response;
12
+use Carbon\Carbon;
13
+use Illuminate\Http\Request;
14
+use Illuminate\Support\Facades\DB;
15
+use Illuminate\Support\Facades\Log;
16
+
17
+/**
18
+ * @group Lottery Prize
19
+ */
20
+class HomePageController extends Controller
21
+{
22
+    public function __construct(
23
+    )
24
+    {
25
+    }
26
+
27
+    public function list($locate = 'tw')
28
+    {
29
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
30
+        $result = [];
31
+
32
+        $webNews = News::where("visible", 1)->orderByDesc("post_date")->limit(3)->get();
33
+        foreach($webNews as $item){
34
+            $result["list"][] = [
35
+                "id" => $item->id,
36
+                "categoryId" => $item->newsCategory->id,
37
+                "category" => $item->newsCategory->getTranslation("name", $locate),
38
+                "postDate" => Carbon::parse($item->post_date)->format("Y.m.d"),
39
+                "title" => $item->getTranslation("title", $locate),
40
+            ];
41
+        }
42
+        return Response::ok($result);
43
+    }
44
+}

+ 116
- 0
app/Http/Controllers/Api/NewsController.php ファイルの表示

@@ -0,0 +1,116 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\News;
7
+use App\Models\NewsCategory;
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
+
15
+/**
16
+ * @group Lottery Prize
17
+ */
18
+class NewsController extends Controller
19
+{
20
+    public function __construct(
21
+    )
22
+    {
23
+    }
24
+
25
+    public function list(Request $request, $locate = 'tw')
26
+    {
27
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
28
+        $request_ordering = $request->ordering ?? "desc";
29
+        $result = [];
30
+
31
+        //年份清單
32
+
33
+        //置頂影音
34
+        $result["top"] = [];
35
+        $top = News::where("visible", 1)->where("on_top", 1)->first();
36
+        if(!empty($top->id))
37
+            $result["top"][] = [
38
+                "id" => $top->id,
39
+                "categoryId" => $top->news_category_id,
40
+                "category" => $top->newsCategory->getTranslation("name", $locate),
41
+                "postDate" => Carbon::parse($top->post_date)->format("Y/m/d"),
42
+                "title" => $top->getTranslation("title", $locate),
43
+                "imgUrl" => $top->news_img_url,
44
+                "imgAlt" => $top->getTranslation("news_img_alt", $locate),
45
+            ];
46
+
47
+        //文章列表
48
+        $news = News::where("visible", 1)->where("on_top", 0);
49
+        $news = $news->orderBy("post_date", $request_ordering)->get();
50
+        foreach($news as $item){
51
+            $result["list"][] = [
52
+                "id" => $item->id,
53
+                "categoryId" => $item->news_category_id,
54
+                "category" => $item->newsCategory->getTranslation("name", $locate),
55
+                "postDate" => Carbon::parse($item->post_date)->format("Y/m/d"),
56
+                "title" => $item->getTranslation("title", $locate),
57
+                "imgUrl" => $item->news_img_url,
58
+                "imgAlt" => $item->getTranslation("news_img_alt", $locate),
59
+            ];
60
+        }
61
+        return Response::ok($result);
62
+    }
63
+
64
+    public function detail($locale = 'tw', $id){
65
+        $locate = $locale == "tw" ? "zh_TW" : $locale;
66
+        $news = News::find($id);
67
+        $paragraphs = [];
68
+        foreach($news->paragraphs as $paragraph){
69
+            $paragraphs[] = [
70
+                "type" => $paragraph->contentType(),
71
+                "imgUrl" => $paragraph->paragraph_img,
72
+                "alt" => $paragraph->getTranslation("img_alt", $locate),
73
+                "imgOutLink" => $paragraph->image_link,
74
+                "content" => nl2br($paragraph->getTranslation("text_content", $locate)),
75
+            ];
76
+        }
77
+
78
+        //瀏覽更多
79
+        $otherNewsList = [];
80
+        $getRangeIds = News::where("visible", 1)->orderByDesc("post_date")->pluck("id")->toArray();
81
+        $helper = new Helper();
82
+        $otherNewsIds = $helper->findArrayTargetIndex($getRangeIds, $id, [-1, 1]);
83
+        $otherNews = News::whereIn('id', $otherNewsIds)->get();
84
+        foreach($otherNews as $item){
85
+            $otherNewsList[] = [
86
+                "id" => $item->id,
87
+                "categoryId" => $item->newsCategory->id,
88
+                "category" => $item->newsCategory->getTranslation("name", $locate),
89
+                "postDate" => Carbon::parse($item->post_date)->format("Y.m.d"),
90
+                "title" => $item->getTranslation("title", $locate),
91
+                "imgUrl" => $item->news_img_url,
92
+                "imgAlt" => $item->getTranslation("news_img_alt", $locate),
93
+                "written" => $item->getTranslation("written_by", $locate),
94
+                "description" => $item->getTranslation("description", $locate),
95
+            ];
96
+        }
97
+        $result = [
98
+            "id" => $news->id,
99
+            "categoryId" => $news->newsCategory->id,
100
+            "category" => $news->newsCategory->getTranslation("name", $locate),
101
+            "postDate" => Carbon::parse($news->post_date)->format("Y.m.d"),
102
+            "title" => $news->getTranslation("title", $locate),
103
+            "imgUrl" => $item->news_img_url,
104
+            "imgAlt" => $item->getTranslation("news_img_alt", $locate),
105
+            "written" => $news->getTranslation("written_by", $locate),
106
+            "description" => $news->getTranslation("description", $locate),
107
+            "metaTitle" => $news->getTranslation("meta_title", $locate),
108
+            "metaKeyword" => $news->getTranslation("meta_keyword", $locate),
109
+            "metaDesc" => $news->getTranslation("meta_description", $locate),
110
+            "metaImg" => $news->meta_img,
111
+            "paragraphs" => $paragraphs,
112
+            "otherNews" => $otherNewsList
113
+        ];
114
+        return Response::ok($result);
115
+    }
116
+}

+ 39
- 0
app/Http/Controllers/Api/ProfilePageController.php ファイルの表示

@@ -0,0 +1,39 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\History;
7
+use App\Models\ProfilePart;
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 ProfilePageController extends Controller
18
+{
19
+    public function __construct(
20
+    )
21
+    {
22
+    }
23
+
24
+    public function list($locate = 'tw')
25
+    {
26
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
27
+        $data = ProfilePart::where("visible", 1)->orderByDesc("order")->get();
28
+        $result = [];
29
+        foreach($data as $item){
30
+            $result["list"][] = [
31
+                "title" => $item->getTranslation("title", $locate),
32
+                "content" => $item->getTranslation("content", $locate),
33
+                "imgUrl" => $item->img_url_link,
34
+                "alt" => $item->getTranslation("img_alt", $locate)
35
+            ];
36
+        }
37
+        return Response::ok($result);
38
+    }
39
+}

+ 126
- 0
app/Http/Controllers/Api/ProjectController.php ファイルの表示

@@ -0,0 +1,126 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\News;
7
+use App\Models\NewsCategory;
8
+use App\Models\Project;
9
+use App\Models\Region;
10
+use App\Models\Tag;
11
+use App\Supports\Response;
12
+use Carbon\Carbon;
13
+use Illuminate\Http\Request;
14
+use Illuminate\Support\Facades\DB;
15
+use Illuminate\Support\Facades\Log;
16
+use App\Http\Helper\Helper;
17
+
18
+/**
19
+ * @group Lottery Prize
20
+ */
21
+class ProjectController extends Controller
22
+{
23
+    public function __construct(
24
+    )
25
+    {
26
+    }
27
+
28
+    public function list(Request $request, $locate = 'tw')
29
+    {
30
+        $categoryId = $request->input("categoryId") ?? "";
31
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
32
+        $result = [];
33
+
34
+        //年份清單
35
+        $regions = Region::where("visible", 1)->pluck("name", "id")->all();
36
+        $tags = Tag::where("visible", 1)->pluck("name", "id")->all();
37
+
38
+        $result["regions"] = $regions;
39
+        $result["tags"] = $tags;
40
+
41
+        //文章列表
42
+        $projects = Project::where("visilbe", 1);
43
+        if($request->has('region')){
44
+            $projects->where("region_id", $request->input('region'));
45
+        }
46
+        if($request->has('tags')){
47
+            $request_tags = $request->input('tags');
48
+            $projects->whereHas("tags", function ($query) use ($request_tags) {
49
+                return $query->whereIn("tags.id", $request_tags);
50
+            });
51
+        }
52
+
53
+        $projects = $projects->with(['tags', 'region'])->orderByDesc("order")->get();
54
+        foreach($projects as $project){
55
+            $result["list"][] = [
56
+                "id" => $project->id,
57
+                "region_id" => $project->region_id,
58
+                "region" => $project->regtion->getTranslation("name", $locate),
59
+                "address" => $project->getTranslation("address", $locate),
60
+                "tags" => $project->tags->pluck('name', 'id')->all(),
61
+                "name" => $project->getTranslation("name", $locate),
62
+                "imgUrl" => $project->first_list_img_url
63
+            ];
64
+        }
65
+        return Response::ok($result);
66
+    }
67
+
68
+    public function detail($locale = 'tw', $id){
69
+
70
+        $locate = $locale == "tw" ? "zh_TW" : $locale;
71
+        $project = Project::find($id);
72
+
73
+        $projectHistories = [];
74
+        foreach($project->histories as $history){
75
+            $operateDate = Carbon::parse($history->operate_date);
76
+            $projectHistories[$operateDate->format("Y")][] = [
77
+                "operateDate" => $operateDate->format("m/d"),
78
+                "title" => $history->getTranslation("title", $locale)
79
+            ];
80
+        }
81
+
82
+        $result = [
83
+            "name" => $project->getTranslation("name", $locale),
84
+            "subName" => $project->getTranslation("sub_name", $locale),
85
+            "description" => $project->getTranslation("description", $locale),
86
+            "images" => $project->img_list,
87
+            "metaTitle" => $project->getTranslation("meta_title", $locate),
88
+            "metaKeyword" => $project->getTranslation("meta_keyword", $locate),
89
+            "metaDesc" => $project->getTranslation("meta_description", $locate),
90
+            "metaImg" => $project->meta_img,
91
+            "region_id" => $project->region_id,
92
+            "region" => $project->region->getTranslation("name", $locale),
93
+            "address" => $project->getTranslation("address", $locale),
94
+            "tags" => $project->tags->pluck("name", "id")->all(),
95
+            "floor_plan" => $project->getTranslation("floor_plan", $locale),
96
+            "building_structure" => $project->getTranslation("building_structure", $locale),
97
+            "design_unit" => $project->getTranslation("design_unit", $locale),
98
+            "badges" => $project->getBadges($locale),
99
+            "contact_info" => [
100
+                "unitName" => $project->getTranslation("contact_unit", $locale),
101
+                "unitPhone" => $project->getTranslation("contact_phone", $locale),
102
+                "inversmentPhone" => $project->getTranslation("inversment_phone", $locale),
103
+            ],
104
+            "spaceInfo" => $project->getTags($locale),
105
+            "historyList" => $projectHistories
106
+        ];
107
+        return Response::ok($result);
108
+    }
109
+
110
+    public function badges($locale = 'tw') {
111
+        $locate = $locale == "tw" ? "zh_TW" : $locale;
112
+
113
+        $projects = Project::where("visible", 1)->get();
114
+        $result = [];
115
+
116
+        foreach($projects as $project){
117
+            $result[] = [
118
+                "id" => $project->id,
119
+                "name" => $project->getTranslation("name", $locale),
120
+                "badges" => $project->getBadges($locale)
121
+            ];
122
+        }
123
+
124
+        return Response::ok($result);
125
+    }
126
+}

+ 41
- 0
app/Http/Helper/Helper.php ファイルの表示

@@ -0,0 +1,41 @@
1
+<?php
2
+
3
+namespace App\Http\Helper;
4
+
5
+class Helper {
6
+
7
+    /**
8
+     * Summary of findNextStepsIDs
9
+     * @param array $arr 目標陣列
10
+     * @param mixed $targetID 指定的value
11
+     * @param mixed $count 尋找組數
12
+     * @return array
13
+     */
14
+    function findArrayTargetIndex(array $arr, $target, mixed $count) {
15
+        // 找到指定value 的Index
16
+        $targetIndex = array_search($target, $arr);
17
+
18
+        $indexCount = count($arr);
19
+
20
+        // 如果指定的value不存在陣列中,則回傳空陣列
21
+        if ($targetIndex === false) {
22
+            return [];
23
+        }
24
+
25
+        $result = [];
26
+        //列出總共想找的序列Index, 正數為後 N 篇 , 負數為前 N 篇
27
+        $loopCount = is_array($count) ? $count : [$count];
28
+        foreach($loopCount as $num){
29
+            for($i = 1; $i <= abs($num); $i++){
30
+                //假設如果往 前/後 已經沒有的話就走循環
31
+                if($num < 0){
32
+                    $result[] = isset($arr[$targetIndex - $i]) ? $arr[$targetIndex - $i] : $arr[$targetIndex - $i + $indexCount];
33
+                }else{
34
+                    $result[] = isset($arr[$targetIndex + $i]) ? $arr[$targetIndex + $i] : $arr[$targetIndex + $i - $indexCount];
35
+                }
36
+            }
37
+        }
38
+
39
+        return $result;
40
+    }
41
+}

+ 2
- 2
app/Models/Badge.php ファイルの表示

@@ -12,13 +12,13 @@ class Badge extends Model
12 12
     use HasTranslations;
13 13
 
14 14
     protected $guarded = ["id"];
15
-    public $translatable = ["title", "img_alt"];
15
+    public $translatable = ["title"];
16 16
     protected $appends = ["img_url_link"];
17 17
 
18 18
     protected function imgUrlLink(): Attribute
19 19
     {
20 20
         return Attribute::make(
21
-            get: fn ($value) => is_null($this->ing_url) ? null :Storage::url($this->ing_url),
21
+            get: fn ($value) => is_null($this->img_url) ? null :Storage::url($this->img_url),
22 22
         );
23 23
     }
24 24
 

+ 17
- 0
app/Models/DeeplGlossary.php ファイルの表示

@@ -0,0 +1,17 @@
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 RalphJSmit\Laravel\SEO\Support\HasSEO;
11
+use App\Models\NewsCategory;
12
+use Spatie\Translatable\HasTranslations;
13
+
14
+class DeeplGlossary extends Model
15
+{
16
+    protected $guarded = ['id'];
17
+}

+ 2
- 2
app/Models/News.php ファイルの表示

@@ -17,7 +17,7 @@ class News extends Model
17 17
     ];
18 18
     protected $guarded = ['id'];
19 19
     protected $appends = ["news_img_url", "meta_img_url"];
20
-    public $translatable = ['title', 'written_by', 'description', 'meta_title', 'meta_description', 'meta_keyword'];
20
+    public $translatable = ['title', 'written_by', 'description', 'news_img_alt', 'meta_title', 'meta_description', 'meta_keyword'];
21 21
 
22 22
     public function newsCategory(){
23 23
         return $this->belongsTo(NewsCategory::class);
@@ -29,7 +29,7 @@ class News extends Model
29 29
     protected function newsImgUrl(): Attribute
30 30
     {
31 31
         return Attribute::make(
32
-            get: fn ($value) => is_null($this->news_imgc) ? null :Storage::url($this->news_img),
32
+            get: fn ($value) => is_null($this->news_img) ? null :Storage::url($this->news_img),
33 33
         );
34 34
     }
35 35
 

+ 10
- 3
app/Models/NewsParagraph.php ファイルの表示

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace App\Models;
4 4
 
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
5 6
 use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Support\Facades\Storage;
6 8
 use Spatie\Translatable\HasTranslations;
7 9
 
8 10
 class NewsParagraph extends Model
@@ -13,6 +15,8 @@ class NewsParagraph extends Model
13 15
 
14 16
     protected $guarded = ['id'];
15 17
     public $timestamps = false;
18
+    protected $appends = ['paragraph_img'];
19
+    public $translatable = ['img_alt', 'text_content'];
16 20
     protected $casts = [
17 21
         'text_content' => 'array',         // JSON 轉陣列
18 22
     ];
@@ -20,6 +24,12 @@ class NewsParagraph extends Model
20 24
     public function news(){
21 25
         return $this->belongsTo(News::class);
22 26
     }
27
+    protected function paragraphImg(): Attribute
28
+    {
29
+        return Attribute::make(
30
+            get: fn ($value) => is_null($this->image_url) ? null :Storage::url($this->image_url),
31
+        );
32
+    }
23 33
 
24 34
     public function contentType()
25 35
     {
@@ -32,9 +42,6 @@ class NewsParagraph extends Model
32 42
                 return "";
33 43
         }
34 44
     }
35
-    public function photos(){
36
-        return $this->hasMany(NewsParagraphPhoto::class)->orderBy("order");
37
-    }
38 45
     /**
39 46
      * 取得特定語言的文字內容
40 47
      */

+ 0
- 28
app/Models/NewsParagraphPhoto.php ファイルの表示

@@ -1,28 +0,0 @@
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 NewsParagraphPhoto extends Model
11
-{
12
-    use HasTranslations;
13
-
14
-    protected $guarded = ['id'];
15
-    public $timestamps = false;
16
-    protected $appends = ['photo_img_url'];
17
-    public $translatable = ['img_alt'];
18
-
19
-    public function paragraph(){
20
-        return $this->belongsTo(NewsParagraph::class);
21
-    }
22
-    protected function photoImgUrl(): Attribute
23
-    {
24
-        return Attribute::make(
25
-            get: fn ($value) => is_null($this->img_url) ? null :Storage::url($this->img_url),
26
-        );
27
-    }
28
-}

+ 70
- 5
app/Models/Project.php ファイルの表示

@@ -2,8 +2,10 @@
2 2
 
3 3
 namespace App\Models;
4 4
 
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
5 6
 use Illuminate\Database\Eloquent\Model;
6 7
 use Illuminate\Database\Eloquent\SoftDeletes;
8
+use Illuminate\Support\Facades\Storage;
7 9
 use Spatie\Translatable\HasTranslations;
8 10
 
9 11
 class Project extends Model
@@ -11,7 +13,17 @@ class Project extends Model
11 13
     use HasTranslations, SoftDeletes;
12 14
     protected $guarded = ['id'];
13 15
 
14
-    protected $translatable = ["title"];
16
+    protected $translatable = ["name", "sub_name", "summaries", "img_alt", "address", "floor_plan",
17
+        "building_structure", "design_unit", "contact_unit", "contact_phone", "inversment_phone"
18
+    ];
19
+    protected $casts = ["img_url" => "array"];
20
+
21
+    protected $appends = ["first_list_img_url", "img_list", "meta_img_url"];
22
+
23
+    public function region()
24
+    {
25
+        return $this->belongsTo(Region::class);
26
+    }
15 27
 
16 28
     public function tags()
17 29
     {
@@ -28,13 +40,66 @@ class Project extends Model
28 40
         return $this->hasMany(ProjectHistory::class);
29 41
     }
30 42
 
31
-    public function summaries()
43
+    public function spaceInfos()
32 44
     {
33
-        return $this->hasMany(ProjectSummary::class);
45
+        return $this->hasMany(ProjectSpaceInfo::class);
34 46
     }
35 47
 
36
-    public function spaceInfos()
48
+    protected function metaImgUrl(): Attribute
37 49
     {
38
-        return $this->hasMany(ProjectSpaceInfo::class);
50
+        return Attribute::make(
51
+            get: fn ($value) => is_null($this->meta_img) ? null :Storage::url($this->meta_img),
52
+        );
53
+    }
54
+
55
+    public function firstListImgUrl(): Attribute
56
+    {
57
+        return Attribute::make(
58
+            get: fn ($value) => is_null($this->img_url) ? null :Storage::url($this->img_url[0]),
59
+        );
60
+    }
61
+
62
+    public function imgList(): Attribute
63
+    {
64
+        $imgList = [];
65
+        if(!is_null($this->img_url) && count($this->img_url) > 0){
66
+            foreach($this->img_url as $img){
67
+                $imgList[] = Storage::url($img);
68
+            }
69
+        }
70
+        return Attribute::make(
71
+            get: fn ($value) => $imgList,
72
+        );
73
+    }
74
+
75
+    public function getBadges($locale) : array
76
+    {
77
+        $badges = [];
78
+        if($this->badges->count() > 0){
79
+            foreach($this->badges as $badge){
80
+                $badges[] = [
81
+                    "imgUrl" => $badge->imgUrlLink,
82
+                    "name" => $badge->getTranslation("name", $locale),
83
+                    "rewardYear" => "取得年份 : " . $badge->reward_year
84
+                ];
85
+            }
86
+        }
87
+
88
+        return $badges;
89
+    }
90
+
91
+    public function getTags($locale) : array
92
+    {
93
+        $tags = [];
94
+        if($this->tags->count() > 0){
95
+            foreach($this->tags as $tag){
96
+                $tags[] = [
97
+                    "id" => $tag->id,
98
+                    "name" => $tag->getTranslation("name", $locale),
99
+                ];
100
+            }
101
+        }
102
+
103
+        return $tags;
39 104
     }
40 105
 }

+ 0
- 15
app/Models/ProjectSummary.php ファイルの表示

@@ -1,15 +0,0 @@
1
-<?php
2
-
3
-namespace App\Models;
4
-
5
-use Illuminate\Database\Eloquent\Model;
6
-use Illuminate\Database\Eloquent\SoftDeletes;
7
-use Spatie\Translatable\HasTranslations;
8
-
9
-class ProjectSummary extends Model
10
-{
11
-    //
12
-    use HasTranslations, SoftDeletes;
13
-    protected $guarded = ["id"];
14
-    protected $translatable = ["title", "content"];
15
-}

+ 4
- 1
app/Providers/Filament/AdminPanelProvider.php ファイルの表示

@@ -19,6 +19,7 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
19 19
 use Illuminate\Routing\Middleware\SubstituteBindings;
20 20
 use Illuminate\Session\Middleware\StartSession;
21 21
 use Illuminate\View\Middleware\ShareErrorsFromSession;
22
+use SolutionForest\FilamentTranslateField\FilamentTranslateFieldPlugin;
22 23
 
23 24
 class AdminPanelProvider extends PanelProvider
24 25
 {
@@ -55,6 +56,8 @@ class AdminPanelProvider extends PanelProvider
55 56
             ->authMiddleware([
56 57
                 Authenticate::class,
57 58
             ])
58
-            ->plugin(SpatieLaravelTranslatablePlugin::make()->defaultLocales(['zh_TW', 'en']));
59
+            ->plugin(
60
+                FilamentTranslateFieldPlugin::make()
61
+                ->defaultLocales(['zh_TW', "en"]));
59 62
     }
60 63
 }

+ 96
- 0
app/Service/DeepLService.php ファイルの表示

@@ -0,0 +1,96 @@
1
+<?php
2
+
3
+namespace App\Service;
4
+use App\Models\DeeplGlossary;
5
+use DeepL\GlossaryEntries;
6
+use Filament\Forms\Components\Actions\Action;
7
+
8
+class DeepLService {
9
+
10
+    protected $mapping_source_lang;
11
+    protected $mapping_trans_lang;
12
+    protected $apiAuthKey,$translator;
13
+
14
+    public function __construct() {
15
+
16
+        $this->mapping_source_lang = [
17
+            "en" => "EN",
18
+            "jp" => "JA",
19
+            "zh_TW" => "ZH"
20
+        ];
21
+        $this->mapping_trans_lang = [
22
+            "en" => "EN-US",
23
+            "jp" => "JA",
24
+            "zh_TW" => "ZH-HANT"
25
+        ];
26
+        $this->apiAuthKey = config("thirdPartyApi.DeepL.api-key"); // Replace with your key
27
+        $this->translator = new \DeepL\Translator($this->apiAuthKey);
28
+    }
29
+
30
+    public function translateWordings($text, $localeTo, $localeFrom = "zh_TW") {
31
+        if(empty($text)){
32
+            return "";
33
+        }
34
+        $authKey = config("thirdPartyApi.DeepL.api-key"); // Replace with your key
35
+        $translator = new \DeepL\Translator($authKey);
36
+
37
+        $glossary = DeeplGlossary::where('translate_to', $localeTo)->first();
38
+
39
+        $result = $translator->translateText($text, $this->mapping_source_lang[$localeFrom], $this->mapping_trans_lang[$localeTo],[
40
+            "glossary" => $glossary->glossary_id
41
+        ]);
42
+        return $result->text; // Bonjour, le monde!
43
+    }
44
+    function createTranslationAction($actionName, array $fieldNames)
45
+    {
46
+        return Action::make("translate{$actionName}")
47
+            ->label("翻譯")
48
+            ->visible(fn ($arguments) => $arguments['locale'] != 'zh_TW')
49
+            ->action(function ($set, $get, $arguments) use ($fieldNames) {
50
+                $locale = $arguments['locale'] ?? null;
51
+                if (!$locale) {
52
+                    return;
53
+                }
54
+                foreach($fieldNames as $fieldName){
55
+                    $translateText = $this->translateWordings($get("{$fieldName}.zh_TW"), $locale);
56
+                    $set("{$fieldName}.{$locale}", $translateText);
57
+                }
58
+            });
59
+    }
60
+
61
+    public function buildGlossary($glossaryName, $glossaryLocaleFrom, $glossaryLocaleTo, $glossaryItems){
62
+        $entries = GlossaryEntries::fromEntries($glossaryItems);
63
+        $locateTo = "";
64
+        switch ($glossaryLocaleTo){
65
+            case "EN":
66
+                $locateTo = "en";
67
+                break;
68
+            case "JA":
69
+                $locateTo = "jp";
70
+                break;
71
+            default:
72
+                break;
73
+        }
74
+        $myGlossary = $this->translator->createGlossary($glossaryName, $glossaryLocaleFrom, $glossaryLocaleTo, $entries);
75
+        DeeplGlossary::insert(["translate_to" => $locateTo,"glossary_name" => $myGlossary->name, "glossary_id" => $myGlossary->glossaryId]);
76
+    }
77
+
78
+    public function listGlossary(){
79
+
80
+        // Find and delete glossaries named 'Old glossary'
81
+        $glossaries = $this->translator->listGlossaries();
82
+        $result = [];
83
+        foreach ($glossaries as $glossary) {
84
+            $myGlossary = $this->translator->getGlossaryEntries($glossary);
85
+            $result[] = $myGlossary->getEntries();
86
+        }
87
+        return $result;
88
+    }
89
+
90
+    public function deleteGlossary(){
91
+        $glossaries = $this->translator->listGlossaries();
92
+        foreach ($glossaries as $glossary) {
93
+            $this->translator->deleteGlossary($glossary);
94
+        }
95
+    }
96
+}

+ 44
- 0
app/Supports/Response.php ファイルの表示

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace App\Supports;
4
+
5
+use Illuminate\Pagination\LengthAwarePaginator;
6
+
7
+class Response
8
+{
9
+    /**
10
+     *    response ok
11
+     *    @param  [type]   $result         [description]
12
+     *    @param  array    $custom         [description]
13
+     *    @param  integer  $responseCode   [description]
14
+     *    @return object                   [description]
15
+     */
16
+    public static function ok($result = [], int $responseCode = 200, array $custom = [])
17
+    {
18
+        $paginator = [];
19
+        if ($result instanceof LengthAwarePaginator) {
20
+            $paginator = collect($result->toArray());
21
+            $result = $paginator->pull('data');
22
+            $paginator = ['paginate' => $paginator];
23
+        }
24
+
25
+        $output = array_merge(['data' => $result], $paginator, $custom);
26
+
27
+        return response()->json($output, $responseCode);
28
+    }
29
+
30
+    /**
31
+     *    response fail
32
+     *    @param  string   $message        [description]
33
+     *    @param  array    $payload        [description]
34
+     *    @param  integer  $responseCode   [description]
35
+     *    @return object                   [description]
36
+     */
37
+    public static function fail($message = 'api error', int $responseCode = 400, array $payload = [])
38
+    {
39
+        return response()->json([
40
+            'message' => $message,
41
+            'errors'  => $payload,
42
+        ], $responseCode);
43
+    }
44
+}

+ 1
- 0
bootstrap/app.php ファイルの表示

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

+ 5
- 2
composer.json ファイルの表示

@@ -9,12 +9,15 @@
9 9
         "php": "^8.2",
10 10
         "abdulmajeed-jamaan/filament-translatable-tabs": "^3.0",
11 11
         "bezhansalleh/filament-shield": "^3.3",
12
+        "deeplcom/deepl-php": "^1.12",
12 13
         "filament/filament": "^3.3",
13
-        "filament/spatie-laravel-translatable-plugin": "^3.2",
14
+        "filament/spatie-laravel-translatable-plugin": "^3.3",
14 15
         "laravel/framework": "^12.0",
16
+        "laravel/sanctum": "^4.0",
15 17
         "laravel/tinker": "^2.10.1",
16 18
         "malzariey/filament-lexical-editor": "^1.1",
17
-        "novadaemon/filament-combobox": "^1.2"
19
+        "novadaemon/filament-combobox": "^1.2",
20
+        "solution-forest/filament-translate-field": "^1.4"
18 21
     },
19 22
     "require-dev": {
20 23
         "fakerphp/faker": "^1.23",

+ 339
- 1
composer.lock ファイルの表示

@@ -4,7 +4,7 @@
4 4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 5
         "This file is @generated automatically"
6 6
     ],
7
-    "content-hash": "23ce7a9a0a953b5acd4695907ed9b85d",
7
+    "content-hash": "e904da9638c3c3831ee33c8e624ce412",
8 8
     "packages": [
9 9
         {
10 10
             "name": "abdulmajeed-jamaan/filament-translatable-tabs",
@@ -621,6 +621,69 @@
621 621
             "time": "2025-02-21T08:52:11+00:00"
622 622
         },
623 623
         {
624
+            "name": "deeplcom/deepl-php",
625
+            "version": "v1.12.0",
626
+            "source": {
627
+                "type": "git",
628
+                "url": "https://github.com/DeepLcom/deepl-php.git",
629
+                "reference": "b9a7e0385d27a99b7756ebf95e953785b90e8161"
630
+            },
631
+            "dist": {
632
+                "type": "zip",
633
+                "url": "https://api.github.com/repos/DeepLcom/deepl-php/zipball/b9a7e0385d27a99b7756ebf95e953785b90e8161",
634
+                "reference": "b9a7e0385d27a99b7756ebf95e953785b90e8161",
635
+                "shasum": ""
636
+            },
637
+            "require": {
638
+                "ext-curl": "*",
639
+                "ext-json": "*",
640
+                "ext-mbstring": "*",
641
+                "php": ">=7.3.0",
642
+                "php-http/discovery": "^1.18",
643
+                "php-http/multipart-stream-builder": "^1.3",
644
+                "psr/http-client": "^1.0",
645
+                "psr/http-client-implementation": "*",
646
+                "psr/http-factory-implementation": "*",
647
+                "psr/log": "^1.1 || ^2.0 || ^3.0"
648
+            },
649
+            "require-dev": {
650
+                "friendsofphp/php-cs-fixer": "^3",
651
+                "guzzlehttp/guzzle": "^7.7.0",
652
+                "php-mock/php-mock-phpunit": "^2.6",
653
+                "phpunit/phpunit": "^9",
654
+                "ramsey/uuid": "^4.2",
655
+                "squizlabs/php_codesniffer": "^3.3"
656
+            },
657
+            "type": "library",
658
+            "autoload": {
659
+                "psr-4": {
660
+                    "DeepL\\": "src/"
661
+                }
662
+            },
663
+            "notification-url": "https://packagist.org/downloads/",
664
+            "license": [
665
+                "MIT"
666
+            ],
667
+            "authors": [
668
+                {
669
+                    "name": "DeepL SE",
670
+                    "email": "open-source@deepl.com"
671
+                }
672
+            ],
673
+            "description": "Official DeepL API Client Library",
674
+            "keywords": [
675
+                "api",
676
+                "deepl",
677
+                "translation",
678
+                "translator"
679
+            ],
680
+            "support": {
681
+                "issues": "https://github.com/DeepLcom/deepl-php/issues",
682
+                "source": "https://github.com/DeepLcom/deepl-php/tree/v1.12.0"
683
+            },
684
+            "time": "2025-04-25T20:00:25+00:00"
685
+        },
686
+        {
624 687
             "name": "dflydev/dot-access-data",
625 688
             "version": "v3.0.3",
626 689
             "source": {
@@ -2511,6 +2574,70 @@
2511 2574
             "time": "2025-07-07T14:17:42+00:00"
2512 2575
         },
2513 2576
         {
2577
+            "name": "laravel/sanctum",
2578
+            "version": "v4.2.0",
2579
+            "source": {
2580
+                "type": "git",
2581
+                "url": "https://github.com/laravel/sanctum.git",
2582
+                "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
2583
+            },
2584
+            "dist": {
2585
+                "type": "zip",
2586
+                "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
2587
+                "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
2588
+                "shasum": ""
2589
+            },
2590
+            "require": {
2591
+                "ext-json": "*",
2592
+                "illuminate/console": "^11.0|^12.0",
2593
+                "illuminate/contracts": "^11.0|^12.0",
2594
+                "illuminate/database": "^11.0|^12.0",
2595
+                "illuminate/support": "^11.0|^12.0",
2596
+                "php": "^8.2",
2597
+                "symfony/console": "^7.0"
2598
+            },
2599
+            "require-dev": {
2600
+                "mockery/mockery": "^1.6",
2601
+                "orchestra/testbench": "^9.0|^10.0",
2602
+                "phpstan/phpstan": "^1.10",
2603
+                "phpunit/phpunit": "^11.3"
2604
+            },
2605
+            "type": "library",
2606
+            "extra": {
2607
+                "laravel": {
2608
+                    "providers": [
2609
+                        "Laravel\\Sanctum\\SanctumServiceProvider"
2610
+                    ]
2611
+                }
2612
+            },
2613
+            "autoload": {
2614
+                "psr-4": {
2615
+                    "Laravel\\Sanctum\\": "src/"
2616
+                }
2617
+            },
2618
+            "notification-url": "https://packagist.org/downloads/",
2619
+            "license": [
2620
+                "MIT"
2621
+            ],
2622
+            "authors": [
2623
+                {
2624
+                    "name": "Taylor Otwell",
2625
+                    "email": "taylor@laravel.com"
2626
+                }
2627
+            ],
2628
+            "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
2629
+            "keywords": [
2630
+                "auth",
2631
+                "laravel",
2632
+                "sanctum"
2633
+            ],
2634
+            "support": {
2635
+                "issues": "https://github.com/laravel/sanctum/issues",
2636
+                "source": "https://github.com/laravel/sanctum"
2637
+            },
2638
+            "time": "2025-07-09T19:45:24+00:00"
2639
+        },
2640
+        {
2514 2641
             "name": "laravel/serializable-closure",
2515 2642
             "version": "v2.0.4",
2516 2643
             "source": {
@@ -4177,6 +4304,141 @@
4177 4304
             "time": "2025-09-03T16:03:54+00:00"
4178 4305
         },
4179 4306
         {
4307
+            "name": "php-http/discovery",
4308
+            "version": "1.20.0",
4309
+            "source": {
4310
+                "type": "git",
4311
+                "url": "https://github.com/php-http/discovery.git",
4312
+                "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
4313
+            },
4314
+            "dist": {
4315
+                "type": "zip",
4316
+                "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
4317
+                "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
4318
+                "shasum": ""
4319
+            },
4320
+            "require": {
4321
+                "composer-plugin-api": "^1.0|^2.0",
4322
+                "php": "^7.1 || ^8.0"
4323
+            },
4324
+            "conflict": {
4325
+                "nyholm/psr7": "<1.0",
4326
+                "zendframework/zend-diactoros": "*"
4327
+            },
4328
+            "provide": {
4329
+                "php-http/async-client-implementation": "*",
4330
+                "php-http/client-implementation": "*",
4331
+                "psr/http-client-implementation": "*",
4332
+                "psr/http-factory-implementation": "*",
4333
+                "psr/http-message-implementation": "*"
4334
+            },
4335
+            "require-dev": {
4336
+                "composer/composer": "^1.0.2|^2.0",
4337
+                "graham-campbell/phpspec-skip-example-extension": "^5.0",
4338
+                "php-http/httplug": "^1.0 || ^2.0",
4339
+                "php-http/message-factory": "^1.0",
4340
+                "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
4341
+                "sebastian/comparator": "^3.0.5 || ^4.0.8",
4342
+                "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
4343
+            },
4344
+            "type": "composer-plugin",
4345
+            "extra": {
4346
+                "class": "Http\\Discovery\\Composer\\Plugin",
4347
+                "plugin-optional": true
4348
+            },
4349
+            "autoload": {
4350
+                "psr-4": {
4351
+                    "Http\\Discovery\\": "src/"
4352
+                },
4353
+                "exclude-from-classmap": [
4354
+                    "src/Composer/Plugin.php"
4355
+                ]
4356
+            },
4357
+            "notification-url": "https://packagist.org/downloads/",
4358
+            "license": [
4359
+                "MIT"
4360
+            ],
4361
+            "authors": [
4362
+                {
4363
+                    "name": "Márk Sági-Kazár",
4364
+                    "email": "mark.sagikazar@gmail.com"
4365
+                }
4366
+            ],
4367
+            "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
4368
+            "homepage": "http://php-http.org",
4369
+            "keywords": [
4370
+                "adapter",
4371
+                "client",
4372
+                "discovery",
4373
+                "factory",
4374
+                "http",
4375
+                "message",
4376
+                "psr17",
4377
+                "psr7"
4378
+            ],
4379
+            "support": {
4380
+                "issues": "https://github.com/php-http/discovery/issues",
4381
+                "source": "https://github.com/php-http/discovery/tree/1.20.0"
4382
+            },
4383
+            "time": "2024-10-02T11:20:13+00:00"
4384
+        },
4385
+        {
4386
+            "name": "php-http/multipart-stream-builder",
4387
+            "version": "1.4.2",
4388
+            "source": {
4389
+                "type": "git",
4390
+                "url": "https://github.com/php-http/multipart-stream-builder.git",
4391
+                "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e"
4392
+            },
4393
+            "dist": {
4394
+                "type": "zip",
4395
+                "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e",
4396
+                "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e",
4397
+                "shasum": ""
4398
+            },
4399
+            "require": {
4400
+                "php": "^7.1 || ^8.0",
4401
+                "php-http/discovery": "^1.15",
4402
+                "psr/http-factory-implementation": "^1.0"
4403
+            },
4404
+            "require-dev": {
4405
+                "nyholm/psr7": "^1.0",
4406
+                "php-http/message": "^1.5",
4407
+                "php-http/message-factory": "^1.0.2",
4408
+                "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3"
4409
+            },
4410
+            "type": "library",
4411
+            "autoload": {
4412
+                "psr-4": {
4413
+                    "Http\\Message\\MultipartStream\\": "src/"
4414
+                }
4415
+            },
4416
+            "notification-url": "https://packagist.org/downloads/",
4417
+            "license": [
4418
+                "MIT"
4419
+            ],
4420
+            "authors": [
4421
+                {
4422
+                    "name": "Tobias Nyholm",
4423
+                    "email": "tobias.nyholm@gmail.com"
4424
+                }
4425
+            ],
4426
+            "description": "A builder class that help you create a multipart stream",
4427
+            "homepage": "http://php-http.org",
4428
+            "keywords": [
4429
+                "factory",
4430
+                "http",
4431
+                "message",
4432
+                "multipart stream",
4433
+                "stream"
4434
+            ],
4435
+            "support": {
4436
+                "issues": "https://github.com/php-http/multipart-stream-builder/issues",
4437
+                "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2"
4438
+            },
4439
+            "time": "2024-09-04T13:22:54+00:00"
4440
+        },
4441
+        {
4180 4442
             "name": "phpoption/phpoption",
4181 4443
             "version": "1.9.4",
4182 4444
             "source": {
@@ -5067,6 +5329,82 @@
5067 5329
             "time": "2025-02-25T09:09:36+00:00"
5068 5330
         },
5069 5331
         {
5332
+            "name": "solution-forest/filament-translate-field",
5333
+            "version": "1.4.1",
5334
+            "source": {
5335
+                "type": "git",
5336
+                "url": "https://github.com/solutionforest/filament-translate-field.git",
5337
+                "reference": "5a920abebfd9ad69495abe50ac75808166fc31ad"
5338
+            },
5339
+            "dist": {
5340
+                "type": "zip",
5341
+                "url": "https://api.github.com/repos/solutionforest/filament-translate-field/zipball/5a920abebfd9ad69495abe50ac75808166fc31ad",
5342
+                "reference": "5a920abebfd9ad69495abe50ac75808166fc31ad",
5343
+                "shasum": ""
5344
+            },
5345
+            "require": {
5346
+                "filament/filament": "^3.0",
5347
+                "illuminate/contracts": ">=10.0",
5348
+                "php": "^8.1",
5349
+                "spatie/laravel-package-tools": "^1.15.0"
5350
+            },
5351
+            "require-dev": {
5352
+                "laravel/pint": "^1.0",
5353
+                "nunomaduro/collision": "^7.9",
5354
+                "nunomaduro/larastan": "^2.0.1",
5355
+                "orchestra/testbench": "^8.0",
5356
+                "pestphp/pest": "^2.0",
5357
+                "pestphp/pest-plugin-arch": "^2.0",
5358
+                "pestphp/pest-plugin-laravel": "^2.0",
5359
+                "phpstan/extension-installer": "^1.1",
5360
+                "phpstan/phpstan-deprecation-rules": "^1.0",
5361
+                "phpstan/phpstan-phpunit": "^1.0",
5362
+                "spatie/laravel-ray": "^1.26"
5363
+            },
5364
+            "type": "library",
5365
+            "extra": {
5366
+                "laravel": {
5367
+                    "aliases": {
5368
+                        "FilamentTranslateField": "SolutionForest\\FilamentTranslateField\\Facades\\FilamentTranslateField"
5369
+                    },
5370
+                    "providers": [
5371
+                        "SolutionForest\\FilamentTranslateField\\FilamentTranslateFieldServiceProvider"
5372
+                    ]
5373
+                }
5374
+            },
5375
+            "autoload": {
5376
+                "psr-4": {
5377
+                    "SolutionForest\\FilamentTranslateField\\": "src/",
5378
+                    "SolutionForest\\FilamentTranslateField\\Database\\Factories\\": "database/factories/"
5379
+                }
5380
+            },
5381
+            "notification-url": "https://packagist.org/downloads/",
5382
+            "license": [
5383
+                "MIT"
5384
+            ],
5385
+            "authors": [
5386
+                {
5387
+                    "name": "Carly",
5388
+                    "email": "info@solutionforest.net",
5389
+                    "role": "Developer"
5390
+                }
5391
+            ],
5392
+            "description": "Filament Translate Field",
5393
+            "homepage": "https://github.com/solution-forest/filament-translate-field",
5394
+            "keywords": [
5395
+                "filament",
5396
+                "filament-translate-field",
5397
+                "laravel",
5398
+                "laravel-translatable",
5399
+                "solution-forest"
5400
+            ],
5401
+            "support": {
5402
+                "issues": "https://github.com/solution-forest/filament-translate-field/issues",
5403
+                "source": "https://github.com/solution-forest/filament-translate-field"
5404
+            },
5405
+            "time": "2025-04-11T10:06:49+00:00"
5406
+        },
5407
+        {
5070 5408
             "name": "spatie/color",
5071 5409
             "version": "1.8.0",
5072 5410
             "source": {

+ 84
- 0
config/sanctum.php ファイルの表示

@@ -0,0 +1,84 @@
1
+<?php
2
+
3
+use Laravel\Sanctum\Sanctum;
4
+
5
+return [
6
+
7
+    /*
8
+    |--------------------------------------------------------------------------
9
+    | Stateful Domains
10
+    |--------------------------------------------------------------------------
11
+    |
12
+    | Requests from the following domains / hosts will receive stateful API
13
+    | authentication cookies. Typically, these should include your local
14
+    | and production domains which access your API via a frontend SPA.
15
+    |
16
+    */
17
+
18
+    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
19
+        '%s%s',
20
+        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
21
+        Sanctum::currentApplicationUrlWithPort(),
22
+        // Sanctum::currentRequestHost(),
23
+    ))),
24
+
25
+    /*
26
+    |--------------------------------------------------------------------------
27
+    | Sanctum Guards
28
+    |--------------------------------------------------------------------------
29
+    |
30
+    | This array contains the authentication guards that will be checked when
31
+    | Sanctum is trying to authenticate a request. If none of these guards
32
+    | are able to authenticate the request, Sanctum will use the bearer
33
+    | token that's present on an incoming request for authentication.
34
+    |
35
+    */
36
+
37
+    'guard' => ['web'],
38
+
39
+    /*
40
+    |--------------------------------------------------------------------------
41
+    | Expiration Minutes
42
+    |--------------------------------------------------------------------------
43
+    |
44
+    | This value controls the number of minutes until an issued token will be
45
+    | considered expired. This will override any values set in the token's
46
+    | "expires_at" attribute, but first-party sessions are not affected.
47
+    |
48
+    */
49
+
50
+    'expiration' => null,
51
+
52
+    /*
53
+    |--------------------------------------------------------------------------
54
+    | Token Prefix
55
+    |--------------------------------------------------------------------------
56
+    |
57
+    | Sanctum can prefix new tokens in order to take advantage of numerous
58
+    | security scanning initiatives maintained by open source platforms
59
+    | that notify developers if they commit tokens into repositories.
60
+    |
61
+    | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
62
+    |
63
+    */
64
+
65
+    'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
66
+
67
+    /*
68
+    |--------------------------------------------------------------------------
69
+    | Sanctum Middleware
70
+    |--------------------------------------------------------------------------
71
+    |
72
+    | When authenticating your first-party SPA with Sanctum you may need to
73
+    | customize some of the middleware Sanctum uses while processing the
74
+    | request. You may change the middleware listed below as required.
75
+    |
76
+    */
77
+
78
+    'middleware' => [
79
+        'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
80
+        'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
81
+        'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
82
+    ],
83
+
84
+];

+ 20
- 0
config/thirdPartyApi.php ファイルの表示

@@ -0,0 +1,20 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Third Party Services
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This file is for storing the credentials for third party services such
11
+    | as Mailgun, Postmark, AWS and more. This file provides the de facto
12
+    | location for this type of information, allowing packages to have
13
+    | a conventional file to locate the various service credentials.
14
+    |
15
+    */
16
+
17
+    'DeepL' => [
18
+        'api-key' => env('DeepL_API_KEY', "39a18a12-0e9d-4037-94e9-24c4b4b81671"),
19
+    ]
20
+];

+ 29
- 0
database/migrations/2024_09_25_043733_create_deepl_glossary_table.php ファイルの表示

@@ -0,0 +1,29 @@
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('deepl_glossaries', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->string('translate_to')->comment("Deepl 翻譯 語系");
17
+            $table->string('glossary_name')->comment("Deepl 翻譯字庫名稱");
18
+            $table->string('glossary_id')->comment("Deepl 特定翻譯字庫ID");
19
+        });
20
+    }
21
+
22
+    /**
23
+     * Reverse the migrations.
24
+     */
25
+    public function down(): void
26
+    {
27
+        Schema::dropIfExists('deepl_glossaries');
28
+    }
29
+};

+ 1
- 0
database/migrations/2025_09_04_073524_create_news_table.php ファイルの表示

@@ -17,6 +17,7 @@ return new class extends Migration
17 17
             $table->id();
18 18
             $table->foreignId('news_category_id')->references('id')->on('news_categories')->comment('文章類別');
19 19
             $table->string('news_img')->nullable()->comment('列表圖');
20
+            $table->json('news_img_alt')->nullable()->comment('列表圖 ALT');
20 21
             $table->json('title')->comment("標題");
21 22
             $table->json('description')->nullable()->comment("短文");
22 23
             $table->json('written_by')->comment("編輯人");

+ 2
- 0
database/migrations/2025_09_17_033657_create_news_paragraphs_table.php ファイルの表示

@@ -18,6 +18,8 @@ return new class extends Migration
18 18
             $table->foreignId('news_id')->references('id')->on('news')->comment("關聯文章");
19 19
             $table->unsignedTinyInteger('paragraph_type')->comment('段落類型 1.images 2.text');
20 20
             $table->json('text_content')->nullable()->comment("文字段落");
21
+            $table->string('img_url')->nullable()->comment("圖片網址");
22
+            $table->json('img_alt')->nullable()->comment("圖片註釋");
21 23
             $table->unsignedInteger('order')->default(0)->comment("排序");
22 24
         });
23 25
         Schema::enableForeignKeyConstraints();

+ 1
- 1
database/migrations/2025_09_17_033900_create_badges_table.php ファイルの表示

@@ -15,7 +15,7 @@ return new class extends Migration
15 15
             $table->id();
16 16
             $table->json("title")->nullable()->comment("標題");
17 17
             $table->string("img_url")->comment("image url");
18
-            $table->json("img_alt")->comment("alt");
18
+            $table->string("reward_year")->comment("image 取得年份");
19 19
             $table->integer("order")->default(0);
20 20
             $table->boolean("visible")->default(1);
21 21
             $table->timestamps();

+ 0
- 34
database/migrations/2025_09_18_033227_create_news_paragraph_photos_table.php ファイルの表示

@@ -1,34 +0,0 @@
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('news_paragraph_photos', function (Blueprint $table) {
17
-            $table->id();
18
-            $table->foreignId('news_paragraph_id')->references('id')->on('news_paragraphs')->comment("關聯文章段落");
19
-            $table->string('img_url')->nullable()->comment("圖片網址");
20
-            $table->json('img_alt')->nullable()->comment("圖片註釋");
21
-            $table->unsignedInteger('order')->default(0)->comment("排序");
22
-        });
23
-
24
-        Schema::enableForeignKeyConstraints();
25
-    }
26
-
27
-    /**
28
-     * Reverse the migrations.
29
-     */
30
-    public function down(): void
31
-    {
32
-        Schema::dropIfExists('news_paragraph_photos');
33
-    }
34
-};

+ 6
- 1
database/migrations/2025_09_18_091406_create_projects_table.php ファイルの表示

@@ -16,11 +16,16 @@ return new class extends Migration
16 16
             $table->id();
17 17
             $table->foreignId('region_id')->references('id')->on('regions')->comment('所屬地區');
18 18
             $table->json("name")->comment("專案項目名稱");
19
+            $table->json("sub_name")->comment("副標名稱");
20
+            $table->json("summaries")->comment("專案概要");
21
+            $table->json('img_url')->nullable()->comment('列表圖');
19 22
             $table->json("address")->comment("基本資料: 地址");
20 23
             $table->json("floor_plan")->comment("樓層規劃");
21 24
             $table->json("building_structure")->comment("建築結構");
22 25
             $table->json("design_unit")->comment("設計團隊");
23
-            $table->json("contact_info")->comment("聯絡資訊");
26
+            $table->json("contact_unit")->comment("物管單位");
27
+            $table->json("contact_phone")->comment("物管電話");
28
+            $table->json("inversment_phone")->comment("招商電話");
24 29
             $table->timestamps();
25 30
             $table->softDeletes();
26 31
         });

+ 3
- 0
database/migrations/2025_09_18_092245_create_regions_table.php ファイルの表示

@@ -26,6 +26,9 @@ return new class extends Migration
26 26
      */
27 27
     public function down(): void
28 28
     {
29
+        Schema::table('regions',function (Blueprint $table) {
30
+            $table->dropForeign('projects_region_id_foreign');
31
+        });
29 32
         Schema::dropIfExists('regions');
30 33
     }
31 34
 };

+ 0
- 37
database/migrations/2025_09_18_092430_create_project_summaries_table.php ファイルの表示

@@ -1,37 +0,0 @@
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('project_summaries', function (Blueprint $table) {
17
-            $table->id();
18
-            $table->foreignId('project_id')->references('id')->on('projects')->comment('所屬專案');
19
-            $table->json("title");
20
-            $table->json("content");
21
-            $table->integer("order")->default(0);
22
-            $table->boolean("visible")->default(true);
23
-            $table->timestamps();
24
-            $table->softDeletes();
25
-        });
26
-
27
-        Schema::enableForeignKeyConstraints();
28
-    }
29
-
30
-    /**
31
-     * Reverse the migrations.
32
-     */
33
-    public function down(): void
34
-    {
35
-        Schema::dropIfExists('project_summaries');
36
-    }
37
-};

+ 33
- 0
database/migrations/2025_10_14_030744_create_personal_access_tokens_table.php ファイルの表示

@@ -0,0 +1,33 @@
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('personal_access_tokens', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->morphs('tokenable');
17
+            $table->text('name');
18
+            $table->string('token', 64)->unique();
19
+            $table->text('abilities')->nullable();
20
+            $table->timestamp('last_used_at')->nullable();
21
+            $table->timestamp('expires_at')->nullable()->index();
22
+            $table->timestamps();
23
+        });
24
+    }
25
+
26
+    /**
27
+     * Reverse the migrations.
28
+     */
29
+    public function down(): void
30
+    {
31
+        Schema::dropIfExists('personal_access_tokens');
32
+    }
33
+};

+ 38
- 0
routes/api.php ファイルの表示

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+use App\Http\Controllers\Api\HistoryController;
4
+use App\Http\Controllers\Api\HomePageController;
5
+use App\Http\Controllers\Api\NewsController;
6
+use App\Http\Controllers\Api\ProfilePageController;
7
+use App\Http\Controllers\Api\ProjectController;
8
+use Illuminate\Http\Request;
9
+use Illuminate\Support\Facades\Route;
10
+
11
+Route::get('/user', function (Request $request) {
12
+    return $request->user();
13
+})->middleware('auth:sanctum');
14
+
15
+
16
+Route::prefix('{locale}')->group(function (){
17
+    Route::prefix('histories')->group(function (){
18
+        Route::get('/', [HistoryController::class, 'list']);
19
+    });
20
+    Route::prefix('home')->group(function (){
21
+        Route::get('/news', [HomePageController::class, 'list']);
22
+    });
23
+
24
+    Route::prefix('profile')->group(function () {
25
+        Route::get('/', [ProfilePageController::class, 'list']);
26
+    });
27
+
28
+    Route::prefix('news')->group(function (){
29
+        Route::get('/', [NewsController::class, 'list']);
30
+        Route::get('/{id}', [NewsController::class, 'detail'])->whereIn('locale', ["tw", "en"])->where('id', '[0-9]+');
31
+    });
32
+
33
+    Route::prefix('project')->group(function () {
34
+        Route::get('/', [ProjectController::class, 'list']);
35
+        Route::get('/{id}', [NewsController::class, 'detail'])->whereIn('locale', ["tw", "en"])->where('id', '[0-9]+');
36
+        Route::get('/badges', [ProjectController::class, 'badges']);
37
+    });
38
+});