[FEAT] 實作機台廣告管理模組與多語系支援
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m7s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m7s
1. 新增廣告管理列表與機台配置介面,包含多語系 (zh_TW, en, ja) 與完整 CRUD 2. 實作基於 Alpine 的廣告素材預覽輪播功能 3. 優化廣告素材下拉選單,強制綁定所屬公司以達成多租戶資料隔離 4. 重構廣告配置中廣告影片的縮圖渲染邏輯,移除 <video> 標籤以大幅提升頁面載入速度與節省頻寬 5. 放寬個人檔案頭像上傳限制,支援 WebP 格式
This commit is contained in:
221
app/Http/Controllers/Admin/AdvertisementController.php
Normal file
221
app/Http/Controllers/Admin/AdvertisementController.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineAdvertisement;
|
||||
use App\Models\System\Advertisement;
|
||||
use App\Models\System\Company;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AdvertisementController extends AdminController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tab = $request->input('tab', 'list');
|
||||
|
||||
// Tab 1: 廣告列表
|
||||
$advertisements = Advertisement::with('company')->latest()->paginate(10);
|
||||
$allAds = Advertisement::active()->get();
|
||||
|
||||
// Tab 2: 機台廣告設置 (所需資料)
|
||||
// 取得使用者有權限的機台列表 (已透過 Global Scope 過濾)
|
||||
$machines = Machine::select('id', 'name', 'serial_no', 'company_id')->get();
|
||||
|
||||
$companies = $user->isSystemAdmin() ? Company::orderBy('name')->get() : collect();
|
||||
|
||||
return view('admin.ads.index', [
|
||||
'advertisements' => $advertisements,
|
||||
'machines' => $machines,
|
||||
'tab' => $tab,
|
||||
'allAds' => $allAds,
|
||||
'companies' => $companies,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 素材 CRUD: 儲存廣告
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:image,video',
|
||||
'duration' => 'required|in:15,30,60',
|
||||
'file' => [
|
||||
'required',
|
||||
'file',
|
||||
'mimes:jpeg,png,jpg,gif,webp,mp4,mov,avi',
|
||||
$request->type === 'image' ? 'max:5120' : 'max:51200', // Image 5MB, Video 50MB
|
||||
],
|
||||
'company_id' => 'nullable|exists:companies,id',
|
||||
]);
|
||||
|
||||
$user = auth()->user();
|
||||
$path = $request->file('file')->store('ads', 'public');
|
||||
|
||||
if ($user->isSystemAdmin()) {
|
||||
$companyId = $request->filled('company_id') ? $request->company_id : null;
|
||||
} else {
|
||||
$companyId = $user->company_id;
|
||||
}
|
||||
|
||||
Advertisement::create([
|
||||
'company_id' => $companyId,
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'duration' => (int) $request->duration,
|
||||
'url' => Storage::disk('public')->url($path),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', __('Advertisement created successfully.'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Advertisement $advertisement)
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:image,video',
|
||||
'duration' => 'required|in:15,30,60',
|
||||
'is_active' => 'boolean',
|
||||
'company_id' => 'nullable|exists:companies,id',
|
||||
];
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
$rules['file'] = [
|
||||
'file',
|
||||
'mimes:jpeg,png,jpg,gif,webp,mp4,mov,avi',
|
||||
$request->type === 'image' ? 'max:5120' : 'max:51200',
|
||||
];
|
||||
}
|
||||
|
||||
$request->validate($rules);
|
||||
|
||||
$data = $request->only(['name', 'type', 'duration']);
|
||||
$data['is_active'] = $request->has('is_active');
|
||||
|
||||
$user = auth()->user();
|
||||
if ($user->isSystemAdmin()) {
|
||||
$data['company_id'] = $request->filled('company_id') ? $request->company_id : null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
// 刪除舊檔案
|
||||
$oldPath = str_replace(Storage::disk('public')->url(''), '', $advertisement->url);
|
||||
Storage::disk('public')->delete($oldPath);
|
||||
|
||||
// 存入新檔案
|
||||
$path = $request->file('file')->store('ads', 'public');
|
||||
$data['url'] = Storage::disk('public')->url($path);
|
||||
}
|
||||
|
||||
$advertisement->update($data);
|
||||
|
||||
return redirect()->back()->with('success', __('Advertisement updated successfully.'));
|
||||
}
|
||||
|
||||
public function destroy(Advertisement $advertisement)
|
||||
{
|
||||
// 檢查是否有機台正投放中
|
||||
if ($advertisement->machineAdvertisements()->exists()) {
|
||||
return redirect()->back()->with('error', __('Cannot delete advertisement being used by machines.'));
|
||||
}
|
||||
|
||||
// 刪除實體檔案
|
||||
$path = str_replace(Storage::disk('public')->url(''), '', $advertisement->url);
|
||||
Storage::disk('public')->delete($path);
|
||||
|
||||
$advertisement->delete();
|
||||
return redirect()->back()->with('success', __('Advertisement deleted successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: 取得特定機台的廣告投放清單
|
||||
*/
|
||||
public function getMachineAds(Machine $machine)
|
||||
{
|
||||
$assignments = MachineAdvertisement::where('machine_id', $machine->id)
|
||||
->with('advertisement')
|
||||
->get()
|
||||
->groupBy('position');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $assignments
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 投放廣告至機台
|
||||
*/
|
||||
public function assign(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
'advertisement_id' => 'required|exists:advertisements,id',
|
||||
'position' => 'required|in:vending,visit_gift,standby',
|
||||
'sort_order' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
// If sort_order is not provided, append to the end of the current position list
|
||||
$newSortOrder = $request->sort_order;
|
||||
if (is_null($newSortOrder)) {
|
||||
$newSortOrder = MachineAdvertisement::where('machine_id', $request->machine_id)
|
||||
->where('position', $request->position)
|
||||
->max('sort_order') + 1;
|
||||
}
|
||||
|
||||
MachineAdvertisement::updateOrCreate(
|
||||
[
|
||||
'machine_id' => $request->machine_id,
|
||||
'position' => $request->position,
|
||||
'advertisement_id' => $request->advertisement_id,
|
||||
],
|
||||
[
|
||||
'sort_order' => $newSortOrder,
|
||||
]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Advertisement assigned successfully.')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新排序廣告播放順序
|
||||
*/
|
||||
public function reorderAssignments(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'assignment_ids' => 'required|array',
|
||||
'assignment_ids.*' => 'exists:machine_advertisements,id'
|
||||
]);
|
||||
|
||||
foreach ($request->assignment_ids as $index => $id) {
|
||||
MachineAdvertisement::where('id', $id)->update(['sort_order' => $index]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Order updated successfully.')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除廣告投放
|
||||
*/
|
||||
public function removeAssignment($id)
|
||||
{
|
||||
$assignment = MachineAdvertisement::findOrFail($id);
|
||||
$assignment->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('Assignment removed successfully.')
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class ProfileController extends Controller
|
||||
public function updateAvatar(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'avatar' => ['required', 'image', 'mimes:jpeg,png,jpg,gif', 'max:1024'],
|
||||
'avatar' => ['required', 'image', 'mimes:jpeg,png,jpg,gif,webp', 'max:1024'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
43
app/Models/Machine/MachineAdvertisement.php
Normal file
43
app/Models/Machine/MachineAdvertisement.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use App\Models\System\Advertisement;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MachineAdvertisement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'advertisement_id',
|
||||
'position',
|
||||
'sort_order',
|
||||
'start_at',
|
||||
'end_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
'start_at' => 'datetime',
|
||||
'end_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the advertisement associated with this assignment.
|
||||
*/
|
||||
public function advertisement()
|
||||
{
|
||||
return $this->belongsTo(Advertisement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the machine associated with this assignment.
|
||||
*/
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
}
|
||||
51
app/Models/System/Advertisement.php
Normal file
51
app/Models/System/Advertisement.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Models\Machine\MachineAdvertisement;
|
||||
use App\Traits\TenantScoped;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Advertisement extends Model
|
||||
{
|
||||
use HasFactory, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'name',
|
||||
'type',
|
||||
'duration',
|
||||
'url',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'duration' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the machine assignments for this advertisement.
|
||||
*/
|
||||
public function machineAdvertisements()
|
||||
{
|
||||
return $this->hasMany(MachineAdvertisement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the company that owns the advertisement.
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include active advertisements.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user