[FEAT] 新增 OTA 預約排程與部署狀態展示,並優化版本排序與 UI
1. 建立 `ota_schedules` 資料表與 `OtaSchedule` Model,提供 OTA 預約排程基礎儲存結構。 2. 建立 `ota:process-schedules` 背景 Artisan Command 並註冊至 Console Kernel,每分鐘自動執行到期的預約並透過 MQTT 發送。 3. 擴充 `ApkVersionController` index 方法:加入加載列表時自動觸發排程的防呆機制,並在記憶體中快速統計 pending 與 completed 的機台。 4. 擴充 `ApkVersionController` store 與 push 方法,移除已用不到的 `is_forced` (更新模式) 限制,並支援 `scheduled_at` 預約時間的寫入。 5. 調整 `create.blade.php` 頁面,移除「強制更新」的 Switch 欄位,僅保留注意事項防錯提示。 6. 調整 `index.blade.php` 頁面,將「更新模式」改為「部署狀態」一欄,整合系統內建的 `.tooltip` 與原生 `title` 屬性,在加載與刷新時 100% 穩定展示 Hover 機台名單。 7. 優化 `index.blade.php` 彈窗,將原生時間輸入框改為 Flatpickr 樣式,加入日曆圖標並支援動態 locale 語言切換(中/英/日),將標籤統一命名為「選擇更新時間」。 8. 同步更新並對齊中、英、日三語系 `lang/*.json` 語系檔,並完成字母排序與反逸出斜線處理。
This commit is contained in:
parent
d9bf2bbe98
commit
fb1bcf49e1
119
app/Console/Commands/Machine/ProcessOtaSchedules.php
Normal file
119
app/Console/Commands/Machine/ProcessOtaSchedules.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Machine;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\Machine\OtaSchedule;
|
||||||
|
use App\Models\Machine\Machine;
|
||||||
|
use App\Models\Machine\RemoteCommand;
|
||||||
|
use App\Services\Machine\MqttService;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ProcessOtaSchedules extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'ota:process-schedules';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Process scheduled OTA firmware updates and send MQTT commands to target machines';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(MqttService $mqttService)
|
||||||
|
{
|
||||||
|
$this->info('[' . now()->toDateTimeString() . '] Starting to process scheduled OTA updates...');
|
||||||
|
|
||||||
|
// 撈取狀態為待發送且時間已到的排程
|
||||||
|
$schedules = OtaSchedule::where('status', 'pending')
|
||||||
|
->where('scheduled_at', '<=', now())
|
||||||
|
->with('apkVersion')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($schedules->isEmpty()) {
|
||||||
|
$this->info('No pending OTA schedules to process.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Found {$schedules->count()} pending OTA schedule(s) to process.");
|
||||||
|
|
||||||
|
foreach ($schedules as $schedule) {
|
||||||
|
$this->info("Processing OTA Schedule ID: {$schedule->id} (APK: {$schedule->apkVersion->version_name})");
|
||||||
|
|
||||||
|
// 標記為處理中,防止重複執行
|
||||||
|
$schedule->update(['status' => 'processing']);
|
||||||
|
$apkVersion = $schedule->apkVersion;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根據 target_type 撈取目標機台
|
||||||
|
if ($schedule->target_type === 'all') {
|
||||||
|
$machines = Machine::all();
|
||||||
|
} else {
|
||||||
|
$machineIds = $schedule->target_value ?? [];
|
||||||
|
$machines = Machine::whereIn('id', $machineIds)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($machines->isEmpty()) {
|
||||||
|
$this->warn("No target machines found for OTA Schedule ID: {$schedule->id}. Skipping.");
|
||||||
|
$schedule->update(['status' => 'completed']);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$successCount = 0;
|
||||||
|
|
||||||
|
foreach ($machines as $machine) {
|
||||||
|
// 1. 先建立 RemoteCommand 紀錄
|
||||||
|
$command = RemoteCommand::create([
|
||||||
|
'machine_id' => $machine->id,
|
||||||
|
'user_id' => $schedule->user_id,
|
||||||
|
'command_type' => 'update_app',
|
||||||
|
'status' => 'pending',
|
||||||
|
'payload' => [
|
||||||
|
'url' => $apkVersion->url,
|
||||||
|
'version' => $apkVersion->version_name,
|
||||||
|
'version_code' => $apkVersion->version_code,
|
||||||
|
'is_forced' => $apkVersion->is_forced,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. 推播 MQTT 更新指令
|
||||||
|
$pushed = $mqttService->pushCommand(
|
||||||
|
$machine->serial_no,
|
||||||
|
'update_app',
|
||||||
|
$command->payload,
|
||||||
|
(string) $command->id
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($pushed) {
|
||||||
|
$successCount++;
|
||||||
|
} else {
|
||||||
|
$command->update(['status' => 'failed']);
|
||||||
|
$this->error("Failed to push MQTT OTA command to machine: {$machine->serial_no}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule->update(['status' => 'completed']);
|
||||||
|
$this->info("Successfully processed OTA Schedule ID: {$schedule->id}. MQTT deployed to {$successCount}/{$machines->count()} machines.");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error("Error processing OTA Schedule ID {$schedule->id}: " . $e->getMessage(), [
|
||||||
|
'exception' => $e
|
||||||
|
]);
|
||||||
|
$schedule->update(['status' => 'failed']);
|
||||||
|
$this->error("Error processing OTA Schedule ID {$schedule->id}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('OTA schedules processing completed.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
|
|||||||
protected function schedule(Schedule $schedule): void
|
protected function schedule(Schedule $schedule): void
|
||||||
{
|
{
|
||||||
// $schedule->command('inspire')->hourly();
|
// $schedule->command('inspire')->hourly();
|
||||||
|
$schedule->command('ota:process-schedules')->everyMinute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,7 +15,39 @@ class ApkVersionController extends AdminController
|
|||||||
{
|
{
|
||||||
public function index(): View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$versions = ApkVersion::orderBy('version_code', 'desc')->paginate(10);
|
// 在進入 APK 版本清單時,同步觸發已到期排程的執行(確保無 cron 的開發/測試環境也能 100% 執行)
|
||||||
|
try {
|
||||||
|
\Illuminate\Support\Facades\Artisan::call('ota:process-schedules');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Auto-trigger scheduled OTA failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$versions = ApkVersion::latest()->paginate(10);
|
||||||
|
|
||||||
|
// 統計並附屬機台部署狀態
|
||||||
|
$commands = \App\Models\Machine\RemoteCommand::where('command_type', 'update_app')
|
||||||
|
->with('machine')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($versions as $version) {
|
||||||
|
$pending = [];
|
||||||
|
$completed = [];
|
||||||
|
|
||||||
|
foreach ($commands as $cmd) {
|
||||||
|
$cmdVersionCode = $cmd->payload['version_code'] ?? null;
|
||||||
|
if ($cmdVersionCode == $version->version_code) {
|
||||||
|
if ($cmd->status === 'completed' || $cmd->status === 'success') {
|
||||||
|
$completed[] = $cmd->machine;
|
||||||
|
} elseif ($cmd->status === 'pending' || $cmd->status === 'processing') {
|
||||||
|
$pending[] = $cmd->machine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$version->pending_machines = collect($pending)->filter()->unique('id');
|
||||||
|
$version->completed_machines = collect($completed)->filter()->unique('id');
|
||||||
|
}
|
||||||
|
|
||||||
// 用於 OTA 下發時讓使用者選擇的機台列表
|
// 用於 OTA 下發時讓使用者選擇的機台列表
|
||||||
$machines = Machine::all();
|
$machines = Machine::all();
|
||||||
return view('admin.basic-settings.apk-versions.index', compact('versions', 'machines'));
|
return view('admin.basic-settings.apk-versions.index', compact('versions', 'machines'));
|
||||||
@ -35,7 +67,6 @@ class ApkVersionController extends AdminController
|
|||||||
'apk_file' => 'required_without:download_url|nullable|file|max:102400', // 上傳限制 100MB
|
'apk_file' => 'required_without:download_url|nullable|file|max:102400', // 上傳限制 100MB
|
||||||
'download_url' => 'required_without:apk_file|nullable|url|max:500',
|
'download_url' => 'required_without:apk_file|nullable|url|max:500',
|
||||||
'release_notes' => 'nullable|string',
|
'release_notes' => 'nullable|string',
|
||||||
'is_forced' => 'nullable|boolean',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$filePath = null;
|
$filePath = null;
|
||||||
@ -50,7 +81,7 @@ class ApkVersionController extends AdminController
|
|||||||
'file_path' => $filePath,
|
'file_path' => $filePath,
|
||||||
'download_url' => $request->download_url,
|
'download_url' => $request->download_url,
|
||||||
'release_notes' => $request->release_notes,
|
'release_notes' => $request->release_notes,
|
||||||
'is_forced' => $request->boolean('is_forced'),
|
'is_forced' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->route('admin.basic-settings.apk-versions.index')
|
return redirect()->route('admin.basic-settings.apk-versions.index')
|
||||||
@ -76,8 +107,24 @@ class ApkVersionController extends AdminController
|
|||||||
$request->validate([
|
$request->validate([
|
||||||
'machine_ids' => 'required|array',
|
'machine_ids' => 'required|array',
|
||||||
'machine_ids.*' => 'exists:machines,id',
|
'machine_ids.*' => 'exists:machines,id',
|
||||||
|
'scheduled_at' => 'nullable|date|after:now',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 如果設定了預約發送時間
|
||||||
|
if ($request->filled('scheduled_at')) {
|
||||||
|
\App\Models\Machine\OtaSchedule::create([
|
||||||
|
'apk_version_id' => $apkVersion->id,
|
||||||
|
'scheduled_at' => $request->scheduled_at,
|
||||||
|
'target_type' => 'custom',
|
||||||
|
'target_value' => $request->machine_ids,
|
||||||
|
'status' => 'pending',
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.basic-settings.apk-versions.index')
|
||||||
|
->with('success', __('OTA update scheduled successfully.'));
|
||||||
|
}
|
||||||
|
|
||||||
$machines = Machine::whereIn('id', $request->machine_ids)->get();
|
$machines = Machine::whereIn('id', $request->machine_ids)->get();
|
||||||
$successCount = 0;
|
$successCount = 0;
|
||||||
|
|
||||||
|
|||||||
35
app/Models/Machine/OtaSchedule.php
Normal file
35
app/Models/Machine/OtaSchedule.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Machine;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class OtaSchedule extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'apk_version_id',
|
||||||
|
'scheduled_at',
|
||||||
|
'target_type',
|
||||||
|
'target_value',
|
||||||
|
'status',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'scheduled_at' => 'datetime',
|
||||||
|
'target_value' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function apkVersion()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ApkVersion::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\System\User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('ota_schedules', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('apk_version_id')->constrained('apk_versions')->onDelete('cascade');
|
||||||
|
$table->dateTime('scheduled_at');
|
||||||
|
$table->string('target_type')->default('custom'); // 'all', 'custom'
|
||||||
|
$table->json('target_value')->nullable(); // 當 target_type = 'custom' 時,儲存 [1, 2, 3] 機台 ID 的 JSON 陣列
|
||||||
|
$table->string('status')->default('pending'); // 'pending', 'processing', 'completed', 'failed'
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null'); // 建立排程的使用者
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('ota_schedules');
|
||||||
|
}
|
||||||
|
};
|
||||||
11
lang/en.json
11
lang/en.json
@ -476,6 +476,7 @@
|
|||||||
"Delta": "Delta",
|
"Delta": "Delta",
|
||||||
"Deploy Update": "Deploy Update",
|
"Deploy Update": "Deploy Update",
|
||||||
"Deploy version": "Deploy version",
|
"Deploy version": "Deploy version",
|
||||||
|
"Deployment Status": "Deployment Status",
|
||||||
"Deposit Bonus": "Deposit Bonus",
|
"Deposit Bonus": "Deposit Bonus",
|
||||||
"Describe the changes in this version...": "Describe the changes in this version...",
|
"Describe the changes in this version...": "Describe the changes in this version...",
|
||||||
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
|
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
|
||||||
@ -527,6 +528,7 @@
|
|||||||
"Dispensing in progress": "Dispensing in progress",
|
"Dispensing in progress": "Dispensing in progress",
|
||||||
"Display Material Code": "Display Material Code",
|
"Display Material Code": "Display Material Code",
|
||||||
"Displaying": "Displaying",
|
"Displaying": "Displaying",
|
||||||
|
"Done": "Done",
|
||||||
"Door Closed": "Door Closed",
|
"Door Closed": "Door Closed",
|
||||||
"Door Opened": "Door Opened",
|
"Door Opened": "Door Opened",
|
||||||
"Download Image": "Download Image",
|
"Download Image": "Download Image",
|
||||||
@ -887,6 +889,7 @@
|
|||||||
"Last Week": "Last Week",
|
"Last Week": "Last Week",
|
||||||
"Latitude": "Latitude",
|
"Latitude": "Latitude",
|
||||||
"Lease": "Lease",
|
"Lease": "Lease",
|
||||||
|
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "Leave blank to deploy immediately, or set a future time to schedule a background release.",
|
||||||
"Leave empty for permanent code": "Leave empty for permanent code",
|
"Leave empty for permanent code": "Leave empty for permanent code",
|
||||||
"Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code",
|
"Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code",
|
||||||
"Leave empty to inherit company settings": "Leave empty to inherit company settings",
|
"Leave empty to inherit company settings": "Leave empty to inherit company settings",
|
||||||
@ -1174,6 +1177,7 @@
|
|||||||
"No machines available": "No machines available",
|
"No machines available": "No machines available",
|
||||||
"No machines available in this company.": "No machines available in this company.",
|
"No machines available in this company.": "No machines available in this company.",
|
||||||
"No machines found": "No machines found",
|
"No machines found": "No machines found",
|
||||||
|
"No machines in this status": "No machines in this status",
|
||||||
"No machines match your criteria": "No machines match your criteria",
|
"No machines match your criteria": "No machines match your criteria",
|
||||||
"No maintenance records found": "No maintenance records found",
|
"No maintenance records found": "No maintenance records found",
|
||||||
"No matching logs found": "No matching logs found",
|
"No matching logs found": "No matching logs found",
|
||||||
@ -1231,6 +1235,7 @@
|
|||||||
"OTA Update Deployment": "OTA Update Deployment",
|
"OTA Update Deployment": "OTA Update Deployment",
|
||||||
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
|
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
|
||||||
"OTA firmware update and version control": "OTA firmware update and version control",
|
"OTA firmware update and version control": "OTA firmware update and version control",
|
||||||
|
"OTA update scheduled successfully.": "OTA update scheduled successfully.",
|
||||||
"Off": "Off",
|
"Off": "Off",
|
||||||
"Offline": "Offline",
|
"Offline": "Offline",
|
||||||
"Offline + 1": "Offline + 1",
|
"Offline + 1": "Offline + 1",
|
||||||
@ -1503,6 +1508,8 @@
|
|||||||
"Quick replenishment from this view": "Quick replenishment from this view",
|
"Quick replenishment from this view": "Quick replenishment from this view",
|
||||||
"Quick search...": "Quick search...",
|
"Quick search...": "Quick search...",
|
||||||
"Rank": "Rank",
|
"Rank": "Rank",
|
||||||
|
"Ready": "Ready",
|
||||||
|
"Ready to Execute": "Ready to Execute",
|
||||||
"Ready to print": "Ready to print",
|
"Ready to print": "Ready to print",
|
||||||
"Real-time OEE analysis awaits": "Real-time OEE analysis awaits",
|
"Real-time OEE analysis awaits": "Real-time OEE analysis awaits",
|
||||||
"Real-time Operation Logs (Last 50)": "Real-time Operation Logs (Last 50)",
|
"Real-time Operation Logs (Last 50)": "Real-time Operation Logs (Last 50)",
|
||||||
@ -1630,6 +1637,7 @@
|
|||||||
"Scan to authorize testing or maintenance": "Scan to authorize testing or maintenance",
|
"Scan to authorize testing or maintenance": "Scan to authorize testing or maintenance",
|
||||||
"Scan to pick up your product": "Scan to pick up your product",
|
"Scan to pick up your product": "Scan to pick up your product",
|
||||||
"Schedule": "Schedule",
|
"Schedule": "Schedule",
|
||||||
|
"Schedule Release Time (Optional)": "Schedule Release Time (Optional)",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Search Company Title...": "Search Company Title...",
|
"Search Company Title...": "Search Company Title...",
|
||||||
"Search Company...": "Search Company...",
|
"Search Company...": "Search Company...",
|
||||||
@ -1695,6 +1703,7 @@
|
|||||||
"Select Slot...": "Select Slot...",
|
"Select Slot...": "Select Slot...",
|
||||||
"Select Target Company": "Select Target Company",
|
"Select Target Company": "Select Target Company",
|
||||||
"Select Target Slot": "Select Target Slot",
|
"Select Target Slot": "Select Target Slot",
|
||||||
|
"Select Update Time (Optional)": "Select Update Time (Optional)",
|
||||||
"Select Warehouse": "Select Warehouse",
|
"Select Warehouse": "Select Warehouse",
|
||||||
"Select a machine to copy settings from...": "Select a machine to copy settings from...",
|
"Select a machine to copy settings from...": "Select a machine to copy settings from...",
|
||||||
"Select a machine to deep dive": "Select a machine to deep dive",
|
"Select a machine to deep dive": "Select a machine to deep dive",
|
||||||
@ -1704,6 +1713,7 @@
|
|||||||
"Select an asset from the left to start analysis": "Select an asset from the left to start analysis",
|
"Select an asset from the left to start analysis": "Select an asset from the left to start analysis",
|
||||||
"Select date to sync data": "Select date to sync data",
|
"Select date to sync data": "Select date to sync data",
|
||||||
"Select flavor...": "Select flavor...",
|
"Select flavor...": "Select flavor...",
|
||||||
|
"Select future time...": "Select update time...",
|
||||||
"Select...": "Select...",
|
"Select...": "Select...",
|
||||||
"Selected": "Selected",
|
"Selected": "Selected",
|
||||||
"Selected Date": "Selected Date",
|
"Selected Date": "Selected Date",
|
||||||
@ -2015,6 +2025,7 @@
|
|||||||
"Unpublish": "Unpublish",
|
"Unpublish": "Unpublish",
|
||||||
"Update": "Update",
|
"Update": "Update",
|
||||||
"Update Authorization": "Update Authorization",
|
"Update Authorization": "Update Authorization",
|
||||||
|
"Update Completed": "Update Completed",
|
||||||
"Update Customer": "Update Customer",
|
"Update Customer": "Update Customer",
|
||||||
"Update Mode": "Update Mode",
|
"Update Mode": "Update Mode",
|
||||||
"Update Pass Code": "Update Pass Code",
|
"Update Pass Code": "Update Pass Code",
|
||||||
|
|||||||
11
lang/ja.json
11
lang/ja.json
@ -476,6 +476,7 @@
|
|||||||
"Delta": "変動量",
|
"Delta": "変動量",
|
||||||
"Deploy Update": "Deploy Update",
|
"Deploy Update": "Deploy Update",
|
||||||
"Deploy version": "Deploy version",
|
"Deploy version": "Deploy version",
|
||||||
|
"Deployment Status": "配信ステータス",
|
||||||
"Deposit Bonus": "チャージボーナス",
|
"Deposit Bonus": "チャージボーナス",
|
||||||
"Describe the changes in this version...": "Describe the changes in this version...",
|
"Describe the changes in this version...": "Describe the changes in this version...",
|
||||||
"Describe the repair or maintenance status...": "修理やメンテナンスの状況を説明してください...",
|
"Describe the repair or maintenance status...": "修理やメンテナンスの状況を説明してください...",
|
||||||
@ -527,6 +528,7 @@
|
|||||||
"Dispensing in progress": "搬送中",
|
"Dispensing in progress": "搬送中",
|
||||||
"Display Material Code": "資材コードを表示",
|
"Display Material Code": "資材コードを表示",
|
||||||
"Displaying": "表示中",
|
"Displaying": "表示中",
|
||||||
|
"Done": "完了",
|
||||||
"Door Closed": "扉が閉まりました",
|
"Door Closed": "扉が閉まりました",
|
||||||
"Door Opened": "扉が開きました",
|
"Door Opened": "扉が開きました",
|
||||||
"Download Image": "画像をダウンロード",
|
"Download Image": "画像をダウンロード",
|
||||||
@ -887,6 +889,7 @@
|
|||||||
"Last Week": "先週",
|
"Last Week": "先週",
|
||||||
"Latitude": "緯度",
|
"Latitude": "緯度",
|
||||||
"Lease": "リース",
|
"Lease": "リース",
|
||||||
|
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "空欄のままにすると即時配信されます。未来の日時を指定すると背景で自動予約配信されます。",
|
||||||
"Leave empty for permanent code": "空欄で無期限コードになります",
|
"Leave empty for permanent code": "空欄で無期限コードになります",
|
||||||
"Leave empty or 0 for permanent code": "空欄または0で無期限コードになります",
|
"Leave empty or 0 for permanent code": "空欄または0で無期限コードになります",
|
||||||
"Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します",
|
"Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します",
|
||||||
@ -1174,6 +1177,7 @@
|
|||||||
"No machines available": "利用可能な機器がありません",
|
"No machines available": "利用可能な機器がありません",
|
||||||
"No machines available in this company.": "この会社で利用可能な機器がありません。",
|
"No machines available in this company.": "この会社で利用可能な機器がありません。",
|
||||||
"No machines found": "機器が見つかりません",
|
"No machines found": "機器が見つかりません",
|
||||||
|
"No machines in this status": "このステータスのデバイスはありません",
|
||||||
"No machines match your criteria": "No machines match your criteria",
|
"No machines match your criteria": "No machines match your criteria",
|
||||||
"No maintenance records found": "メンテナンス記録が見つかりません",
|
"No maintenance records found": "メンテナンス記録が見つかりません",
|
||||||
"No matching logs found": "一致するログが見つかりません",
|
"No matching logs found": "一致するログが見つかりません",
|
||||||
@ -1231,6 +1235,7 @@
|
|||||||
"OTA Update Deployment": "OTA Update Deployment",
|
"OTA Update Deployment": "OTA Update Deployment",
|
||||||
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
|
"OTA command sent to %d devices successfully.": "OTA command sent to %d devices successfully.",
|
||||||
"OTA firmware update and version control": "OTA firmware update and version control",
|
"OTA firmware update and version control": "OTA firmware update and version control",
|
||||||
|
"OTA update scheduled successfully.": "OTA予約更新スケジュールが正常に作成されました。",
|
||||||
"Off": "オフ",
|
"Off": "オフ",
|
||||||
"Offline": "オフライン",
|
"Offline": "オフライン",
|
||||||
"Offline + 1": "オフライン + 1",
|
"Offline + 1": "オフライン + 1",
|
||||||
@ -1503,6 +1508,8 @@
|
|||||||
"Quick replenishment from this view": "この画面からクイック補充",
|
"Quick replenishment from this view": "この画面からクイック補充",
|
||||||
"Quick search...": "クイック検索...",
|
"Quick search...": "クイック検索...",
|
||||||
"Rank": "順位",
|
"Rank": "順位",
|
||||||
|
"Ready": "準備中",
|
||||||
|
"Ready to Execute": "コマンド実行待ちデバイス",
|
||||||
"Ready to print": "印刷準備完了",
|
"Ready to print": "印刷準備完了",
|
||||||
"Real-time OEE analysis awaits": "リアルタイムOEE分析待機中",
|
"Real-time OEE analysis awaits": "リアルタイムOEE分析待機中",
|
||||||
"Real-time Operation Logs (Last 50)": "リアルタイム操作ログ (直近50件)",
|
"Real-time Operation Logs (Last 50)": "リアルタイム操作ログ (直近50件)",
|
||||||
@ -1630,6 +1637,7 @@
|
|||||||
"Scan to authorize testing or maintenance": "スキャンしてテストまたはメンテナンスを承認します",
|
"Scan to authorize testing or maintenance": "スキャンしてテストまたはメンテナンスを承認します",
|
||||||
"Scan to pick up your product": "スキャンして商品をお受け取りください",
|
"Scan to pick up your product": "スキャンして商品をお受け取りください",
|
||||||
"Schedule": "スケジュール",
|
"Schedule": "スケジュール",
|
||||||
|
"Schedule Release Time (Optional)": "予約配信日時(任意)",
|
||||||
"Search": "検索",
|
"Search": "検索",
|
||||||
"Search Company Title...": "会社名を検索...",
|
"Search Company Title...": "会社名を検索...",
|
||||||
"Search Company...": "会社を検索...",
|
"Search Company...": "会社を検索...",
|
||||||
@ -1695,6 +1703,7 @@
|
|||||||
"Select Slot...": "スロットを選択...",
|
"Select Slot...": "スロットを選択...",
|
||||||
"Select Target Company": "対象の会社を選択",
|
"Select Target Company": "対象の会社を選択",
|
||||||
"Select Target Slot": "対象のスロットを選択",
|
"Select Target Slot": "対象のスロットを選択",
|
||||||
|
"Select Update Time (Optional)": "更新時間を選択してください(任意)",
|
||||||
"Select Warehouse": "倉庫を選択",
|
"Select Warehouse": "倉庫を選択",
|
||||||
"Select a machine to copy settings from...": "設定のコピー元機器を選択...",
|
"Select a machine to copy settings from...": "設定のコピー元機器を選択...",
|
||||||
"Select a machine to deep dive": "詳細分析する機器を選択",
|
"Select a machine to deep dive": "詳細分析する機器を選択",
|
||||||
@ -1704,6 +1713,7 @@
|
|||||||
"Select an asset from the left to start analysis": "左側から機器を選択して分析を開始",
|
"Select an asset from the left to start analysis": "左側から機器を選択して分析を開始",
|
||||||
"Select date to sync data": "データを同期する日付を選択",
|
"Select date to sync data": "データを同期する日付を選択",
|
||||||
"Select flavor...": "Select flavor...",
|
"Select flavor...": "Select flavor...",
|
||||||
|
"Select future time...": "配信日時を選択してください...",
|
||||||
"Select...": "選択してください...",
|
"Select...": "選択してください...",
|
||||||
"Selected": "選択中",
|
"Selected": "選択中",
|
||||||
"Selected Date": "選択日",
|
"Selected Date": "選択日",
|
||||||
@ -2015,6 +2025,7 @@
|
|||||||
"Unpublish": "非公開",
|
"Unpublish": "非公開",
|
||||||
"Update": "更新",
|
"Update": "更新",
|
||||||
"Update Authorization": "権限更新",
|
"Update Authorization": "権限更新",
|
||||||
|
"Update Completed": "アップデート完了デバイス",
|
||||||
"Update Customer": "顧客更新",
|
"Update Customer": "顧客更新",
|
||||||
"Update Mode": "Update Mode",
|
"Update Mode": "Update Mode",
|
||||||
"Update Pass Code": "パスコード更新",
|
"Update Pass Code": "パスコード更新",
|
||||||
|
|||||||
@ -476,6 +476,7 @@
|
|||||||
"Delta": "變化量",
|
"Delta": "變化量",
|
||||||
"Deploy Update": "部署更新",
|
"Deploy Update": "部署更新",
|
||||||
"Deploy version": "部署版本",
|
"Deploy version": "部署版本",
|
||||||
|
"Deployment Status": "部署狀態",
|
||||||
"Deposit Bonus": "儲值回饋",
|
"Deposit Bonus": "儲值回饋",
|
||||||
"Describe the changes in this version...": "描述此版本的變更...",
|
"Describe the changes in this version...": "描述此版本的變更...",
|
||||||
"Describe the repair or maintenance status...": "請描述維修或保養狀況...",
|
"Describe the repair or maintenance status...": "請描述維修或保養狀況...",
|
||||||
@ -527,6 +528,7 @@
|
|||||||
"Dispensing in progress": "正在出貨中",
|
"Dispensing in progress": "正在出貨中",
|
||||||
"Display Material Code": "顯示物料代碼",
|
"Display Material Code": "顯示物料代碼",
|
||||||
"Displaying": "目前顯示",
|
"Displaying": "目前顯示",
|
||||||
|
"Done": "已完成",
|
||||||
"Door Closed": "機門已關閉",
|
"Door Closed": "機門已關閉",
|
||||||
"Door Opened": "機門已開啟",
|
"Door Opened": "機門已開啟",
|
||||||
"Download Image": "下載圖片",
|
"Download Image": "下載圖片",
|
||||||
@ -887,6 +889,7 @@
|
|||||||
"Last Week": "上週",
|
"Last Week": "上週",
|
||||||
"Latitude": "緯度",
|
"Latitude": "緯度",
|
||||||
"Lease": "租賃",
|
"Lease": "租賃",
|
||||||
|
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "留空將立即部署更新,或設定未來時間由背景自動排程發布。",
|
||||||
"Leave empty for permanent code": "留空表示永久有效",
|
"Leave empty for permanent code": "留空表示永久有效",
|
||||||
"Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼",
|
"Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼",
|
||||||
"Leave empty to inherit company settings": "留空則繼承公司全域設定",
|
"Leave empty to inherit company settings": "留空則繼承公司全域設定",
|
||||||
@ -1174,6 +1177,7 @@
|
|||||||
"No machines available": "目前沒有可供分配的機台",
|
"No machines available": "目前沒有可供分配的機台",
|
||||||
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
|
"No machines available in this company.": "此客戶目前沒有可供分配的機台。",
|
||||||
"No machines found": "未找到機台",
|
"No machines found": "未找到機台",
|
||||||
|
"No machines in this status": "此狀態下無設備",
|
||||||
"No machines match your criteria": "沒有符合條件的機台",
|
"No machines match your criteria": "沒有符合條件的機台",
|
||||||
"No maintenance records found": "找不到維修紀錄",
|
"No maintenance records found": "找不到維修紀錄",
|
||||||
"No matching logs found": "找不到符合條件的日誌",
|
"No matching logs found": "找不到符合條件的日誌",
|
||||||
@ -1231,6 +1235,7 @@
|
|||||||
"OTA Update Deployment": "OTA 更新部署",
|
"OTA Update Deployment": "OTA 更新部署",
|
||||||
"OTA command sent to %d devices successfully.": "OTA 指令已成功發送至 %d 台設備。",
|
"OTA command sent to %d devices successfully.": "OTA 指令已成功發送至 %d 台設備。",
|
||||||
"OTA firmware update and version control": "OTA 韌體更新與版本控制",
|
"OTA firmware update and version control": "OTA 韌體更新與版本控制",
|
||||||
|
"OTA update scheduled successfully.": "OTA 預約更新排程已建立成功。",
|
||||||
"Off": "關閉",
|
"Off": "關閉",
|
||||||
"Offline": "離線",
|
"Offline": "離線",
|
||||||
"Offline + 1": "離線 + 1",
|
"Offline + 1": "離線 + 1",
|
||||||
@ -1503,6 +1508,8 @@
|
|||||||
"Quick replenishment from this view": "從此畫面快速補貨",
|
"Quick replenishment from this view": "從此畫面快速補貨",
|
||||||
"Quick search...": "快速搜尋...",
|
"Quick search...": "快速搜尋...",
|
||||||
"Rank": "排行",
|
"Rank": "排行",
|
||||||
|
"Ready": "準備中",
|
||||||
|
"Ready to Execute": "準備要執行指令的設備",
|
||||||
"Ready to print": "準備列印",
|
"Ready to print": "準備列印",
|
||||||
"Real-time OEE analysis awaits": "即時 OEE 分析預備中",
|
"Real-time OEE analysis awaits": "即時 OEE 分析預備中",
|
||||||
"Real-time Operation Logs (Last 50)": "即時操作日誌 (最後 50 筆)",
|
"Real-time Operation Logs (Last 50)": "即時操作日誌 (最後 50 筆)",
|
||||||
@ -1630,6 +1637,7 @@
|
|||||||
"Scan to authorize testing or maintenance": "掃描以進行測試或維護授權",
|
"Scan to authorize testing or maintenance": "掃描以進行測試或維護授權",
|
||||||
"Scan to pick up your product": "掃描以領取您的商品",
|
"Scan to pick up your product": "掃描以領取您的商品",
|
||||||
"Schedule": "排程區間",
|
"Schedule": "排程區間",
|
||||||
|
"Schedule Release Time (Optional)": "預約發布時間(選填)",
|
||||||
"Search": "搜尋",
|
"Search": "搜尋",
|
||||||
"Search Company Title...": "搜尋公司名稱...",
|
"Search Company Title...": "搜尋公司名稱...",
|
||||||
"Search Company...": "搜尋公司...",
|
"Search Company...": "搜尋公司...",
|
||||||
@ -1695,6 +1703,7 @@
|
|||||||
"Select Slot...": "選擇貨道...",
|
"Select Slot...": "選擇貨道...",
|
||||||
"Select Target Company": "選擇目標公司",
|
"Select Target Company": "選擇目標公司",
|
||||||
"Select Target Slot": "選擇目標貨道",
|
"Select Target Slot": "選擇目標貨道",
|
||||||
|
"Select Update Time (Optional)": "選擇更新時間(選填)",
|
||||||
"Select Warehouse": "選擇倉庫",
|
"Select Warehouse": "選擇倉庫",
|
||||||
"Select a machine to copy settings from...": "選擇要複製設定的來源機台...",
|
"Select a machine to copy settings from...": "選擇要複製設定的來源機台...",
|
||||||
"Select a machine to deep dive": "請選擇機台以開始深度分析",
|
"Select a machine to deep dive": "請選擇機台以開始深度分析",
|
||||||
@ -1704,6 +1713,7 @@
|
|||||||
"Select an asset from the left to start analysis": "選擇左側機台以開始分析數據",
|
"Select an asset from the left to start analysis": "選擇左側機台以開始分析數據",
|
||||||
"Select date to sync data": "選擇日期以同步數據",
|
"Select date to sync data": "選擇日期以同步數據",
|
||||||
"Select flavor...": "選擇風味...",
|
"Select flavor...": "選擇風味...",
|
||||||
|
"Select future time...": "選擇更新時間",
|
||||||
"Select...": "請選擇...",
|
"Select...": "請選擇...",
|
||||||
"Selected": "已選擇",
|
"Selected": "已選擇",
|
||||||
"Selected Date": "查詢日期",
|
"Selected Date": "查詢日期",
|
||||||
@ -2015,6 +2025,7 @@
|
|||||||
"Unpublish": "下架",
|
"Unpublish": "下架",
|
||||||
"Update": "編輯",
|
"Update": "編輯",
|
||||||
"Update Authorization": "更新授權",
|
"Update Authorization": "更新授權",
|
||||||
|
"Update Completed": "已經完成更新指令的設備",
|
||||||
"Update Customer": "更新客戶",
|
"Update Customer": "更新客戶",
|
||||||
"Update Mode": "更新模式",
|
"Update Mode": "更新模式",
|
||||||
"Update Pass Code": "更新通行碼",
|
"Update Pass Code": "更新通行碼",
|
||||||
|
|||||||
@ -173,49 +173,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- 更新模式卡片 --}}
|
{{-- 注意事項卡片 --}}
|
||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card" style="animation-delay: 100ms">
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in group/card bg-amber-500/5 dark:bg-amber-500/5 border border-amber-500/15" style="animation-delay: 100ms">
|
||||||
<div class="flex items-center gap-5 mb-8">
|
<div class="flex items-start gap-4">
|
||||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/10 to-orange-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 group-hover/card:scale-110 transition-transform duration-500">
|
<div class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-500 border border-amber-500/20 shadow-lg shadow-amber-500/5 shrink-0">
|
||||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
|
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
|
||||||
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight font-display uppercase">{{ __('Update Mode') }}</h3>
|
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1 font-display">{{ __('Important Notice') }}</p>
|
||||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Control deployment behavior') }}</p>
|
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- 強制更新開關 --}}
|
|
||||||
<div class="flex items-center justify-between p-5 rounded-2xl border transition-all duration-300"
|
|
||||||
:class="isForced
|
|
||||||
? 'bg-rose-500/10 dark:bg-rose-500/10 border-rose-500/20 dark:border-rose-500/30 text-rose-600 dark:text-rose-400 shadow-sm shadow-rose-500/5'
|
|
||||||
: 'bg-slate-50/80 dark:bg-slate-800/40 border-slate-100 dark:border-slate-800/80 text-slate-700 dark:text-slate-200'">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300"
|
|
||||||
:class="isForced ? 'bg-rose-500/10 text-rose-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
|
|
||||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-extrabold transition-colors"
|
|
||||||
:class="isForced ? 'text-rose-600 dark:text-rose-400' : 'text-slate-700 dark:text-slate-200'">{{ __('Forced Update') }}</p>
|
|
||||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 tracking-wide mt-0.5">{{ __('During OTA deployment, the device will install directly in the background without user confirmation.') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label class="relative inline-flex items-center cursor-pointer shrink-0">
|
|
||||||
<input type="hidden" name="is_forced" value="0">
|
|
||||||
<input type="checkbox" name="is_forced" value="1" x-model="isForced" class="sr-only peer" {{ old('is_forced') ? 'checked' : '' }}>
|
|
||||||
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-rose-500"></div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- 注意事項 --}}
|
|
||||||
<div class="mt-6 p-5 rounded-2xl bg-amber-500/10 border border-amber-500/20 shadow-sm shadow-amber-500/5">
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<svg class="w-5 h-5 stroke-[2.5] text-amber-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
|
|
||||||
<div class="text-xs font-bold text-amber-800 dark:text-amber-300 tracking-wide leading-relaxed">
|
|
||||||
<p class="text-sm font-black uppercase tracking-widest mb-1.5 flex items-center gap-1">{{ __('Important Notice') }}</p>
|
|
||||||
<p class="font-bold opacity-90 leading-relaxed">{{ __('Version Code must be strictly greater than the previous release, otherwise the OTA silent installation on the device will fail with INSTALL_FAILED_ALREADY_EXISTS.') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -230,7 +196,6 @@
|
|||||||
uploadMode: 'file',
|
uploadMode: 'file',
|
||||||
fileName: '',
|
fileName: '',
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isForced: false,
|
|
||||||
versionName: '{{ old('version_name', '') }}',
|
versionName: '{{ old('version_name', '') }}',
|
||||||
versionCode: '{{ old('version_code', '') }}',
|
versionCode: '{{ old('version_code', '') }}',
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,9 @@
|
|||||||
this.searchMachineQuery = '';
|
this.searchMachineQuery = '';
|
||||||
this.onlyOnlineMachines = false;
|
this.onlyOnlineMachines = false;
|
||||||
this.isPushModalOpen = true;
|
this.isPushModalOpen = true;
|
||||||
|
// 清空預約時間
|
||||||
|
const scheduleInput = document.getElementById('scheduled_at');
|
||||||
|
if (scheduleInput) scheduleInput.value = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
get filteredMachines() {
|
get filteredMachines() {
|
||||||
@ -143,7 +146,7 @@
|
|||||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Version Info') }}</th>
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Version Info') }}</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Flavor') }}</th>
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Flavor') }}</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Update Mode') }}</th>
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Deployment Status') }}</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Release Notes') }}</th>
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Release Notes') }}</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
|
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -171,16 +174,46 @@
|
|||||||
{{ $version->flavor }}
|
{{ $version->flavor }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6">
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
@if($version->is_forced)
|
<div class="flex items-center gap-3">
|
||||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-rose-100 dark:border-rose-900/30 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 tracking-widest uppercase">
|
<!-- 準備中 -->
|
||||||
{{ __('Forced Update') }}
|
@php
|
||||||
</span>
|
$pendingTooltip = __('Ready to Execute') . ":\n";
|
||||||
@else
|
if ($version->pending_machines->isEmpty()) {
|
||||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-slate-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 tracking-widest uppercase">
|
$pendingTooltip .= "• " . __('No machines in this status');
|
||||||
{{ __('Optional Update') }}
|
} else {
|
||||||
</span>
|
foreach($version->pending_machines as $m) {
|
||||||
@endif
|
$pendingTooltip .= "• {$m->name} ({$m->serial_no})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pendingTooltip = trim($pendingTooltip);
|
||||||
|
@endphp
|
||||||
|
<div class="inline-block tooltip cursor-help" title="{{ $pendingTooltip }}">
|
||||||
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-amber-100 dark:border-amber-900/30 bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400 tracking-widest inline-flex items-center gap-1.5">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
|
||||||
|
{{ __('Ready') }}: {{ $version->pending_machines->count() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已完成 -->
|
||||||
|
@php
|
||||||
|
$completedTooltip = __('Update Completed') . ":\n";
|
||||||
|
if ($version->completed_machines->isEmpty()) {
|
||||||
|
$completedTooltip .= "• " . __('No machines in this status');
|
||||||
|
} else {
|
||||||
|
foreach($version->completed_machines as $m) {
|
||||||
|
$completedTooltip .= "• {$m->name} ({$m->serial_no})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$completedTooltip = trim($completedTooltip);
|
||||||
|
@endphp
|
||||||
|
<div class="inline-block tooltip cursor-help" title="{{ $completedTooltip }}">
|
||||||
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-emerald-100 dark:border-emerald-900/30 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 tracking-widest inline-flex items-center gap-1.5">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||||
|
{{ __('Done') }}: {{ $version->completed_machines->count() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-6 max-w-xs truncate">
|
<td class="px-6 py-6 max-w-xs truncate">
|
||||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed">
|
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||||
@ -307,6 +340,42 @@
|
|||||||
<!-- 表單與機台選擇清單 -->
|
<!-- 表單與機台選擇清單 -->
|
||||||
<form :action="pushActionUrl" method="POST" id="ota-push-form">
|
<form :action="pushActionUrl" method="POST" id="ota-push-form">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
|
<!-- 預約排程時間設定 -->
|
||||||
|
<div class="mb-6 p-4 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/20">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="scheduled_at" class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5">
|
||||||
|
{{ __('Select Update Time (Optional)') }}
|
||||||
|
</label>
|
||||||
|
<p class="text-[10px] text-slate-400 dark:text-slate-500 leading-normal">
|
||||||
|
{{ __('Leave blank to deploy immediately, or set a future time to schedule a background release.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-64 group">
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input type="text"
|
||||||
|
id="scheduled_at"
|
||||||
|
name="scheduled_at"
|
||||||
|
x-init="flatpickr($el, {
|
||||||
|
enableTime: true,
|
||||||
|
dateFormat: 'Y-m-d H:i',
|
||||||
|
time_24hr: true,
|
||||||
|
locale: '{{ app()->getLocale() === 'zh_TW' ? 'zh_tw' : (app()->getLocale() === 'ja' ? 'ja' : 'en') }}',
|
||||||
|
disableMobile: true
|
||||||
|
})"
|
||||||
|
class="luxury-input py-2.5 pl-12 pr-4 block w-full text-sm font-bold tracking-tight cursor-pointer"
|
||||||
|
placeholder="{{ __('Select future time...') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="border border-slate-100 dark:border-slate-800/80 rounded-2xl overflow-hidden">
|
<div class="border border-slate-100 dark:border-slate-800/80 rounded-2xl overflow-hidden">
|
||||||
<table class="w-full text-left">
|
<table class="w-full text-left">
|
||||||
<thead class="bg-slate-50/50 dark:bg-slate-950/40">
|
<thead class="bg-slate-50/50 dark:bg-slate-950/40">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user