[FEAT] 完善 IoT API 規范化、機台管理介面優化與 B005 改為 GET
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s
1. 將 B005 (廣告同步) 從 POST 改為 GET,符合 RESTful 規範。
2. 完善 B009 (庫存回報) 回應規格,加入業務代碼 (200 OK)。
3. API 文件 UI 優化:新增 Method Badge (方法標籤),並修正 JSON 中文/斜線轉義問題。
4. 機台管理介面優化:實作「唯讀庫存與效期」面板,並將日誌圖示改為「👁️」。
5. 標準化 ID 識別邏輯:資料表全面移除對 sku 的依賴,改以 id 為主、barcode 為輔。
6. 新增 Migration:正式移除 sku 欄位並同步 barcode 指向。
7. 更新多語系支援 (zh_TW, en, ja)。
This commit is contained in:
@@ -41,7 +41,68 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
||||
|
||||
---
|
||||
|
||||
### 3.2 B010: 心跳上報與狀態同步
|
||||
### 3.2 B005: 廣告清單同步
|
||||
用於機台端獲取目前應播放的廣告檔案 URL 清單。
|
||||
|
||||
- **URL**: `GET /api/v1/app/machine/ad/B005`
|
||||
- **Request Body:** 無 (GET 請求)
|
||||
|
||||
- **Response Body:**
|
||||
| 參數 | 類型 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `success` | Boolean | 請求是否成功 | `true` |
|
||||
| `code` | Integer | 內部業務狀態碼 | `200` |
|
||||
| `data` | Array | 廣告物件陣列 | `[{"t070v04": "https://..."}]` |
|
||||
|
||||
**data 陣列內部欄位:**
|
||||
- `t070v01`: 廣告名稱 (Name)
|
||||
- `t070v02`: 播放長度 (Duration) — 秒數,若後台未設定,預設為 15 秒。
|
||||
- `t070v03`: 廣告位置 (Position/Flag) — (`3`: 待機廣告, `1`: 販賣頁, `2`: 來店禮)。
|
||||
- `t070v04`: 廣告 URL。
|
||||
- `t070v05`: 播放順位 (Sort Order)。
|
||||
|
||||
---
|
||||
|
||||
### 3.3 B009: 貨道庫存即時回報 (Supplementary Report)
|
||||
當維修或補貨人員在機台端完成操作後,將目前的貨道實體狀態同步回雲端。
|
||||
|
||||
#### B009 權限驗證邏輯 (RBAC Compliance)
|
||||
系統會依據 `account` 欄位進行三層式權限核查:
|
||||
1. **系統層 (System Admin)**:當 `company_id` 為 `null` 時,具備全局管理權限,直接放行。
|
||||
2. **公司層 (Company Admin)**:當 `is_admin` 為 `true` 時,檢查機台的 `company_id` 是否與該帳號一致。
|
||||
3. **人員層 (Operator/User)**:當帳號僅為一般人員時,檢查 `machine_user` 授權表,確認該帳號有被分配至此機台。
|
||||
|
||||
- **URL**: `PUT /api/v1/app/products/supplementary/B009`
|
||||
- **Request Body:**
|
||||
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `account` | String | 是 | 操作人員帳號 | `0999123456` |
|
||||
| `data` | Array | 是 | 貨道數據陣列 | `[{"tid":"1", "t060v00":"1", "num":"10"}]` |
|
||||
|
||||
- **data 陣列內部欄位:**
|
||||
- `tid`: 貨道編號 (Slot No)
|
||||
- `t060v00`: **商品資料庫 ID**
|
||||
- `num`: 實體剩餘庫存數量
|
||||
|
||||
- **Response Body (Success 200):**
|
||||
| 參數 | 類型 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `success` | Boolean | 同步是否成功 | `true` |
|
||||
| `code` | Integer | 內部業務狀態碼 | `200` |
|
||||
| `message` | String | 回應訊息 | `Slot report synchronized success` |
|
||||
| `status` | String | 固定回傳 `49` 代表同步完成 | `49` |
|
||||
|
||||
- **Response Body (Forbidden 403):**
|
||||
| 參數 | 類型 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `success` | Boolean | 同步失敗 | `false` |
|
||||
| `status` | String | 回傳空字串 `""` 防止觸發指令 | `""` |
|
||||
| `code` | Integer | HTTP 狀態碼 | `403` |
|
||||
| `message` | String | 錯誤訊息 | `Unauthorized` |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 B010: 心跳上報與狀態同步
|
||||
用於確認機台在線狀態、更新感測數據、提交事件日誌並獲取雲端指令。
|
||||
|
||||
- **URL**: `POST /api/v1/app/machine/status/B010`
|
||||
@@ -85,60 +146,3 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
||||
- `71`: lock (鎖定)
|
||||
- `85`: reload B0552 (遠端出貨)
|
||||
- `待定義`: change (遠端找零 - 註:指令中心有此功能,但目前 Java App 尚無對接對應的連動事件)
|
||||
|
||||
---
|
||||
|
||||
### 3.3 B017: 貨道與庫存同步
|
||||
- **URL**: `POST /api/v1/app/machine/reload_msg/B017`
|
||||
- **說明**:當機台收到 B010 回應 `status: 49` 時,呼叫此此 API 獲取雲端最新的貨道佈局與庫存設定。
|
||||
|
||||
---
|
||||
|
||||
### 3.4 B005: 廣告清單同步
|
||||
用於機台端獲取目前應播放的廣告檔案 URL 清單。
|
||||
|
||||
- **URL**: `POST /api/v1/app/machine/ad/B005`
|
||||
- **Request Body:** (無須額外 Body 參數,僅需傳送空 JSON `{}`)
|
||||
|
||||
- **Response Body:**
|
||||
| 參數 | 類型 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `success` | Boolean | 請求是否成功 | `true` |
|
||||
| `data` | Array | 廣告物件陣列 | `[{"t070v04": "https://..."}]` |
|
||||
|
||||
**data 陣列內部欄位:**
|
||||
- `t070v01`: 廣告名稱 (Name)
|
||||
- `t070v02`: 播放長度 (Duration) — 秒數,若後台未設定,預設為 15 秒。
|
||||
- `t070v03`: 廣告位置 (Position/Flag) — (`3`: 待機廣告, `1`: 販賣頁, `2`: 來店禮)。
|
||||
- `t070v04`: 廣告 URL。
|
||||
- `t070v05`: 播放順位 (Sort Order)。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 B009: 貨道庫存即時回報 (Supplementary Report)
|
||||
當維修或補貨人員在機台端完成操作後,將目前的貨道實體狀態同步回雲端。
|
||||
|
||||
- **URL**: `PUT /api/v1/app/products/supplementary/B009`
|
||||
- **Request Body:**
|
||||
| 參數 | 類型 | 必填 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `account` | String | 是 | 操作人員帳號 | `technician_01` |
|
||||
| `vmcType` | String | 否 | VMC 韌體/機型類別 | `XinYuan` |
|
||||
| `data` | Array | 貨道數據陣列 | `[{"tid":"1", "t060v00":"SKU001", "num":"10"}]` |
|
||||
|
||||
- **data 陣列內部欄位:**
|
||||
- `tid`: 貨道編號 (Slot No)
|
||||
- `t060v00`: 商品編碼 (SKU / Product Code)
|
||||
- `num`: 實體剩餘庫存數量
|
||||
|
||||
- **Response Body:**
|
||||
| 參數 | 類型 | 說明 | 範例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `success` | Boolean | 同步是否成功 | `true` |
|
||||
| `status` | String | 固定回傳 `49` 代表已處理 | `49` |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 B600: 交易數據回傳 (規劃中)
|
||||
- **URL**: `POST /api/v1/app/B600`
|
||||
- 說明:交易完成後提交支付方式、金額、商品與出貨結果。
|
||||
|
||||
@@ -5,9 +5,11 @@ namespace App\Http\Controllers\Api\V1\App;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\System\User;
|
||||
use App\Jobs\Machine\ProcessHeartbeat;
|
||||
use App\Jobs\Machine\ProcessTimerStatus;
|
||||
use App\Jobs\Machine\ProcessCoinInventory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class MachineController extends Controller
|
||||
@@ -203,9 +205,11 @@ class MachineController extends Controller
|
||||
$machine = $request->get('machine');
|
||||
|
||||
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
|
||||
->with(['advertisement' => function ($query) {
|
||||
->with([
|
||||
'advertisement' => function ($query) {
|
||||
$query->active();
|
||||
}])
|
||||
}
|
||||
])
|
||||
->get()
|
||||
->filter(fn($ma) => $ma->advertisement !== null)
|
||||
->map(function ($ma) {
|
||||
@@ -241,7 +245,7 @@ class MachineController extends Controller
|
||||
['raw_sort', 'asc']
|
||||
])
|
||||
->values()
|
||||
->map(function($item) {
|
||||
->map(function ($item) {
|
||||
unset($item['raw_pos_weight'], $item['raw_sort']);
|
||||
return $item;
|
||||
});
|
||||
@@ -260,10 +264,51 @@ class MachineController extends Controller
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$payload = $request->all();
|
||||
$account = $payload['account'] ?? null;
|
||||
|
||||
// 映射舊版機台回傳格式 (Map legacy machine format)
|
||||
// t060v00 -> product_id, num -> stock, tid -> slot_no
|
||||
// 1. 驗證帳號是否存在 (驗證執行補貨的人員身分)
|
||||
$user = User::where('username', $account)
|
||||
->orWhere('email', $account)
|
||||
->first();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Unauthorized: Account not found',
|
||||
'status' => ''
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 2. 階層式權限驗證 (遵循 RBAC 多租戶規範)
|
||||
$isAuthorized = false;
|
||||
if ($user->isSystemAdmin()) {
|
||||
// [系統層]:系統管理員可異動所有機台
|
||||
$isAuthorized = true;
|
||||
} elseif ($user->is_admin) {
|
||||
// [公司層]:公司管理員需驗證此機台是否隸屬於該公司
|
||||
$isAuthorized = ($machine->company_id === $user->company_id);
|
||||
} else {
|
||||
// [人員層]:一般人員需檢查 machine_user 授權表
|
||||
$isAuthorized = $user->machines()->where('machine_id', $machine->id)->exists();
|
||||
}
|
||||
|
||||
if (!$isAuthorized) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Unauthorized: Account not authorized for this machine',
|
||||
'status' => ''
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 3. 映射舊版機台回傳格式 (Map legacy machine format)
|
||||
// 支持單個物件 data: {} 或 陣列 data: [{}] (Handle both single object and array)
|
||||
$legacyData = $payload['data'] ?? [];
|
||||
if (Arr::isAssoc($legacyData)) {
|
||||
$legacyData = [$legacyData];
|
||||
}
|
||||
|
||||
$mappedSlots = array_map(function ($item) {
|
||||
return [
|
||||
'slot_no' => $item['tid'] ?? null,
|
||||
|
||||
@@ -16,7 +16,6 @@ class Product extends Model
|
||||
'category_id',
|
||||
'name',
|
||||
'name_dictionary_key',
|
||||
'sku',
|
||||
'barcode',
|
||||
'spec',
|
||||
'manufacturer',
|
||||
|
||||
@@ -14,7 +14,7 @@ class OrderItem extends Model
|
||||
'order_id',
|
||||
'product_id',
|
||||
'product_name',
|
||||
'sku',
|
||||
'barcode',
|
||||
'price',
|
||||
'quantity',
|
||||
'subtotal',
|
||||
|
||||
@@ -67,11 +67,10 @@ class MachineService
|
||||
// 蒐集所有傳入的商品 ID (可能是 SKU 或 實際 ID)
|
||||
$productCodes = collect($slotsData)->pluck('product_id')->filter()->unique()->toArray();
|
||||
|
||||
// 批次查詢商品 (支援以 SKU 查詢,確保對應至資料庫 ID)
|
||||
$products = \App\Models\Product\Product::whereIn('sku', $productCodes)
|
||||
->orWhereIn('id', $productCodes)
|
||||
->get()
|
||||
->keyBy(fn($p) => $p->sku ?: $p->id);
|
||||
// 優先以 ID 查詢商品,若 ID 不存在則嘗試 Barcode (Prioritize ID lookup, fallback to Barcode)
|
||||
$products = \App\Models\Product\Product::whereIn('id', $productCodes)
|
||||
->orWhereIn('barcode', $productCodes)
|
||||
->get();
|
||||
|
||||
foreach ($slotsData as $slotData) {
|
||||
$slotNo = $slotData['slot_no'] ?? null;
|
||||
@@ -79,11 +78,13 @@ class MachineService
|
||||
|
||||
$existingSlot = $machine->slots()->where('slot_no', $slotNo)->first();
|
||||
|
||||
// 查找對應的實體 ID
|
||||
// 查找對應的實體 ID (支援 ID 與 Barcode 比對)
|
||||
$productCode = $slotData['product_id'] ?? null;
|
||||
$actualProductId = null;
|
||||
if ($productCode) {
|
||||
$actualProductId = $products->get($productCode)?->id;
|
||||
$actualProductId = $products->first(function ($p) use ($productCode) {
|
||||
return (string)$p->id === (string)$productCode || $p->barcode === (string)$productCode;
|
||||
})?->id;
|
||||
}
|
||||
|
||||
$updateData = [
|
||||
|
||||
@@ -42,7 +42,7 @@ class TransactionService
|
||||
$order->items()->create([
|
||||
'product_id' => $item['product_id'],
|
||||
'product_name' => $item['product_name'] ?? 'Unknown',
|
||||
'sku' => $item['sku'] ?? null,
|
||||
'barcode' => $item['barcode'] ?? null,
|
||||
'price' => $item['price'],
|
||||
'quantity' => $item['quantity'],
|
||||
'subtotal' => $item['price'] * $item['quantity'],
|
||||
|
||||
@@ -8,6 +8,119 @@ return [
|
||||
[
|
||||
'name' => '機台核心通訊 (IoT Core)',
|
||||
'apis' => [
|
||||
[
|
||||
'name' => 'B005: 廣告清單同步 (Ad Sync)',
|
||||
'slug' => 'b005-ad-sync',
|
||||
'method' => 'GET',
|
||||
'path' => '/api/v1/app/machine/ad/B005',
|
||||
'description' => '用於機台端獲取目前應播放的廣告檔案 URL 清單。此介面無需 Request Body。',
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer <api_token>',
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'parameters' => [],
|
||||
'response_parameters' => [
|
||||
'success' => [
|
||||
'type' => 'boolean',
|
||||
'description' => '請求是否成功',
|
||||
'example' => true
|
||||
],
|
||||
'code' => [
|
||||
'type' => 'integer',
|
||||
'description' => '內部業務狀態碼',
|
||||
'example' => 200
|
||||
],
|
||||
'data' => [
|
||||
'type' => 'array',
|
||||
'description' => '廣告物件陣列。內部欄位包含:t070v01 (名稱), t070v02 (秒數), t070v03 (位置:1:販賣頁, 2:來店禮, 3:待機廣告), t070v04 (URL), t070v05 (順位)',
|
||||
'example' => [
|
||||
[
|
||||
't070v01' => '測試機台廣告',
|
||||
't070v02' => 15,
|
||||
't070v03' => 3,
|
||||
't070v04' => 'https://example.com/ad1.mp4',
|
||||
't070v05' => 1
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
'request' => [],
|
||||
'response' => [
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
'data' => [
|
||||
[
|
||||
't070v01' => '測試機台廣告',
|
||||
't070v02' => 15,
|
||||
't070v03' => 3,
|
||||
't070v04' => 'https://example.com/ad1.mp4',
|
||||
't070v05' => 1
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'B009: 貨道庫存即時回報 (Inventory Report)',
|
||||
'slug' => 'b009-inventory-report',
|
||||
'method' => 'PUT',
|
||||
'path' => '/api/v1/app/products/supplementary/B009',
|
||||
'description' => '當人員在機台端完成操作後,將目前的貨道實體狀態同步回雲端。需進行 RBAC 權限核查。',
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer <api_token>',
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'parameters' => [
|
||||
'account' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '操作人員帳號',
|
||||
'example' => '0999123456'
|
||||
],
|
||||
'data' => [
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'description' => '貨道數據陣列。tid: 貨道號, t060v00: 商品 ID, num: 庫存量',
|
||||
'example' => [
|
||||
['tid' => '1', 't060v00' => '1', 'num' => '10']
|
||||
]
|
||||
],
|
||||
],
|
||||
'response_parameters' => [
|
||||
'success' => [
|
||||
'type' => 'boolean',
|
||||
'description' => '同步是否成功',
|
||||
'example' => true
|
||||
],
|
||||
'code' => [
|
||||
'type' => 'integer',
|
||||
'description' => '內部業務狀態碼',
|
||||
'example' => 200
|
||||
],
|
||||
'message' => [
|
||||
'type' => 'string',
|
||||
'description' => '回應訊息',
|
||||
'example' => 'Slot report synchronized success'
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'string',
|
||||
'description' => '固定回傳 49 代表同步完成',
|
||||
'example' => '49'
|
||||
],
|
||||
],
|
||||
'request' => [
|
||||
'account' => '0999123456',
|
||||
'data' => [
|
||||
['tid' => '1', 't060v00' => '1', 'num' => '10']
|
||||
]
|
||||
],
|
||||
'response' => [
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Slot report synchronized success',
|
||||
'status' => '49'
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'B010: 心跳上報與狀態同步 (Heartbeat)',
|
||||
'slug' => 'b010-heartbeat',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 從 products 表移除 sku
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
// 因為公司外鍵正在使用這個複合索引,必須先補一個獨立索引給公司
|
||||
$table->index('company_id');
|
||||
// 現在可以安全移除含有 sku 的索引了
|
||||
$table->dropIndex(['company_id', 'sku']);
|
||||
$table->dropColumn('sku');
|
||||
});
|
||||
|
||||
// 2. 從 order_items 表移除 sku 並新增 barcode
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->dropColumn('sku');
|
||||
$table->string('barcode')->nullable()->after('product_name')->comment('商品條碼 (備份)');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->dropColumn('barcode');
|
||||
$table->string('sku')->nullable()->after('product_name')->comment('商品編號 (備份)');
|
||||
});
|
||||
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('sku')->nullable()->after('name_dictionary_key')->comment('商品編號');
|
||||
$table->index(['company_id', 'sku']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -615,6 +615,7 @@
|
||||
"No roles available": "No roles available",
|
||||
"No roles found.": "No roles found.",
|
||||
"No slots found": "No slots found",
|
||||
"No slot data available": "No slot data available",
|
||||
"No users found": "No users found",
|
||||
"None": "None",
|
||||
"Normal": "Normal",
|
||||
@@ -904,6 +905,7 @@
|
||||
"Stock": "Stock",
|
||||
"Stock & Expiry": "Stock & Expiry",
|
||||
"Stock & Expiry Management": "Stock & Expiry Management",
|
||||
"Stock & Expiry Overview": "Stock & Expiry Overview",
|
||||
"Stock Management": "Stock Management",
|
||||
"Stock Quantity": "Stock Quantity",
|
||||
"Stock:": "Stock:",
|
||||
@@ -1025,6 +1027,7 @@
|
||||
"Venue Management": "Venue Management",
|
||||
"video": "video",
|
||||
"View Details": "View Details",
|
||||
"View Inventory": "View Inventory",
|
||||
"View Logs": "View Logs",
|
||||
"View More": "View More",
|
||||
"Visit Gift": "Visit Gift",
|
||||
|
||||
@@ -615,6 +615,7 @@
|
||||
"No roles available": "利用可能な権限はありません",
|
||||
"No roles found.": "権限が見つかりません。",
|
||||
"No slots found": "スロットが見つかりません",
|
||||
"No slot data available": "スロットデータがありません",
|
||||
"No users found": "ユーザーが見つかりません",
|
||||
"None": "なし",
|
||||
"Normal": "通常",
|
||||
@@ -904,6 +905,7 @@
|
||||
"Stock": "在庫",
|
||||
"Stock & Expiry": "在庫と消費期限",
|
||||
"Stock & Expiry Management": "在庫・期限管理",
|
||||
"Stock & Expiry Overview": "在庫・期限一覧",
|
||||
"Stock Management": "在庫管理",
|
||||
"Stock Quantity": "在庫数",
|
||||
"Stock:": "在庫:",
|
||||
@@ -1025,6 +1027,7 @@
|
||||
"Venue Management": "会場管理",
|
||||
"video": "video",
|
||||
"View Details": "詳細を見る",
|
||||
"View Inventory": "在庫を見る",
|
||||
"View Logs": "ログを見る",
|
||||
"View More": "もっと見る",
|
||||
"Visit Gift": "来店ギフト",
|
||||
|
||||
@@ -615,6 +615,7 @@
|
||||
"No roles available": "目前沒有角色資料。",
|
||||
"No roles found.": "找不到角色資料。",
|
||||
"No slots found": "未找到貨道資訊",
|
||||
"No slot data available": "尚無貨道資料",
|
||||
"No users found": "找不到用戶資料",
|
||||
"None": "無",
|
||||
"Normal": "正常",
|
||||
@@ -904,6 +905,7 @@
|
||||
"Stock": "庫存",
|
||||
"Stock & Expiry": "庫存與效期",
|
||||
"Stock & Expiry Management": "庫存與效期管理",
|
||||
"Stock & Expiry Overview": "庫存與效期一覽",
|
||||
"Stock Management": "庫存管理單",
|
||||
"Stock Quantity": "庫存數量",
|
||||
"Stock:": "庫存:",
|
||||
@@ -1025,6 +1027,7 @@
|
||||
"Venue Management": "場地管理",
|
||||
"video": "影片",
|
||||
"View Details": "查看詳情",
|
||||
"View Inventory": "查看庫存",
|
||||
"View Logs": "查看日誌",
|
||||
"View More": "查看更多",
|
||||
"Visit Gift": "來店禮",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
return {
|
||||
showLogPanel: false,
|
||||
showEditModal: false,
|
||||
showInventoryPanel: false,
|
||||
editMachineId: '',
|
||||
editMachineName: '',
|
||||
activeTab: 'status',
|
||||
@@ -15,12 +16,14 @@
|
||||
currentMachineName: '',
|
||||
logs: [],
|
||||
loading: false,
|
||||
inventoryLoading: false,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
tab: 'list',
|
||||
viewMode: 'fleet',
|
||||
selectedMachine: null,
|
||||
slots: [],
|
||||
inventorySlots: [],
|
||||
|
||||
init() {
|
||||
const d = new Date();
|
||||
@@ -79,11 +82,43 @@
|
||||
finally { this.loading = false; }
|
||||
},
|
||||
|
||||
// 庫存一覽面板 (唯讀)
|
||||
async openInventoryPanel(id, sn, name) {
|
||||
this.currentMachineId = id;
|
||||
this.currentMachineSn = sn;
|
||||
this.currentMachineName = name;
|
||||
this.inventorySlots = [];
|
||||
this.showInventoryPanel = true;
|
||||
this.inventoryLoading = true;
|
||||
try {
|
||||
const res = await fetch('/admin/machines/' + id + '/slots-ajax');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.inventorySlots = data.slots;
|
||||
}
|
||||
} catch (e) { console.error('openInventoryPanel error:', e); }
|
||||
finally { this.inventoryLoading = false; }
|
||||
},
|
||||
|
||||
getSlotColorClass(slot) {
|
||||
if (!slot.expiry_date) return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50';
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
const expiryStr = slot.expiry_date;
|
||||
if (expiryStr < todayStr) {
|
||||
return 'bg-rose-50/60 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-500/30 shadow-sm shadow-rose-500/5';
|
||||
}
|
||||
const diffDays = Math.round((new Date(expiryStr) - new Date(todayStr)) / 86400000);
|
||||
if (diffDays <= 7) {
|
||||
return 'bg-amber-50/60 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-500/30 shadow-sm shadow-amber-500/5';
|
||||
}
|
||||
return 'bg-emerald-50/60 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/30 shadow-sm shadow-emerald-500/5';
|
||||
},
|
||||
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()" @keydown.escape.window="showLogPanel = false">
|
||||
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()" @keydown.escape.window="showLogPanel = false; showInventoryPanel = false">
|
||||
<!-- Top Header & Actions -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -243,6 +278,16 @@
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button type="button"
|
||||
@click="openInventoryPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
||||
title="{{ __('View Inventory') }}">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
||||
@@ -260,7 +305,9 @@
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -571,4 +618,194 @@
|
||||
</div>
|
||||
</div><!-- /Edit Modal -->
|
||||
|
||||
<!-- Inventory Offcanvas Panel (唯讀庫存一覽) -->
|
||||
<div x-show="showInventoryPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
|
||||
aria-labelledby="inventory-panel-title" role="dialog" aria-modal="true">
|
||||
|
||||
<!-- Background backdrop -->
|
||||
<div x-show="showInventoryPanel" x-transition:enter="ease-in-out duration-300" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in-out duration-300"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showInventoryPanel = false">
|
||||
</div>
|
||||
|
||||
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
||||
<!-- Sliding panel -->
|
||||
<div x-show="showInventoryPanel"
|
||||
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
||||
class="w-screen max-w-4xl">
|
||||
|
||||
<div class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl">
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="px-5 py-6 sm:px-8 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 id="inventory-panel-title"
|
||||
class="text-xl sm:text-2xl font-black text-slate-800 dark:text-white font-display flex items-center gap-2 sm:gap-3">
|
||||
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-cyan-500 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<span class="truncate">{{ __('Stock & Expiry Overview') }}</span>
|
||||
</h2>
|
||||
<div
|
||||
class="mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-[10px] sm:text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
|
||||
<span x-text="currentMachineSn"
|
||||
class="font-mono text-cyan-600 dark:text-cyan-400 truncate"></span>
|
||||
<span class="hidden sm:inline opacity-50">—</span>
|
||||
<span x-text="currentMachineName" class="truncate"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0 h-7 flex items-center">
|
||||
<button type="button" @click="showInventoryPanel = false"
|
||||
class="bg-white dark:bg-slate-800 rounded-full p-2 text-slate-400 hover:text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition duration-300 shadow-sm border border-slate-200 dark:border-slate-700">
|
||||
<span class="sr-only">{{ __('Close Panel') }}</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 統計摘要 -->
|
||||
<div class="mt-6 flex items-center gap-4">
|
||||
<div class="px-5 py-3 rounded-2xl bg-white dark:bg-slate-800/50 flex flex-col items-center min-w-[100px] border border-slate-100 dark:border-slate-800/50">
|
||||
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5">{{ __('Total Slots') }}</span>
|
||||
<span class="text-2xl font-black text-slate-700 dark:text-slate-200" x-text="inventorySlots.length"></span>
|
||||
</div>
|
||||
<div class="px-5 py-3 rounded-2xl bg-rose-500/5 border border-rose-500/10 flex flex-col items-center min-w-[100px]">
|
||||
<span class="text-[9px] font-black text-rose-500 uppercase tracking-widest mb-0.5">{{ __('Low Stock') }}</span>
|
||||
<span class="text-2xl font-black text-rose-600" x-text="inventorySlots.filter(s => s != null && s.stock <= 5).length"></span>
|
||||
</div>
|
||||
<div class="px-5 py-3 rounded-2xl bg-amber-500/5 border border-amber-500/10 flex flex-col items-center min-w-[100px]">
|
||||
<span class="text-[9px] font-black text-amber-500 uppercase tracking-widest mb-0.5">{{ __('Expiring') }}</span>
|
||||
<span class="text-2xl font-black text-amber-600" x-text="inventorySlots.filter(s => { if (!s || !s.expiry_date) return false; const diff = Math.round((new Date(s.expiry_date) - new Date()) / 86400000); return diff >= 0 && diff <= 7; }).length"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /Header -->
|
||||
|
||||
<!-- Body / Cabinet Grid -->
|
||||
<div class="flex-1 overflow-y-auto p-6 sm:p-8">
|
||||
<div class="relative min-h-[400px]">
|
||||
<!-- Loading State -->
|
||||
<div x-show="inventoryLoading"
|
||||
class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 backdrop-blur-[1px] flex items-center justify-center z-20">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
</div>
|
||||
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
|
||||
__('Loading...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Legend -->
|
||||
<div class="flex items-center gap-6 mb-6" x-show="!inventoryLoading">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-rose-500 shadow-lg shadow-rose-500/30"></span>
|
||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Expired') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
|
||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Warning') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
|
||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{ __('Normal') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slots Grid (唯讀) -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-5" x-show="!inventoryLoading">
|
||||
<template x-for="slot in inventorySlots" :key="slot.id">
|
||||
<div :class="getSlotColorClass(slot)"
|
||||
class="min-h-[260px] rounded-[2rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-300 relative">
|
||||
|
||||
<!-- Slot Header -->
|
||||
<div class="absolute top-3.5 left-4 right-4 flex justify-between items-center z-10">
|
||||
<div class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
|
||||
<span class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white" x-text="slot.slot_no"></span>
|
||||
</div>
|
||||
<template x-if="slot.stock <= 2">
|
||||
<div class="px-2 py-1 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
||||
{{ __('Low') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Product Image -->
|
||||
<div class="relative w-16 h-16 mb-3 mt-2">
|
||||
<div class="absolute inset-0 rounded-2xl bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner overflow-hidden">
|
||||
<template x-if="slot.product && slot.product.image_url">
|
||||
<img :src="slot.product.image_url" class="w-full h-full object-cover">
|
||||
</template>
|
||||
<template x-if="!slot.product || !slot.product.image_url">
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<svg class="w-7 h-7 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slot Info -->
|
||||
<div class="text-center w-full space-y-2">
|
||||
<template x-if="slot.product">
|
||||
<div class="text-sm font-black truncate w-full opacity-90 tracking-tight" x-text="slot.product.name"></div>
|
||||
</template>
|
||||
<template x-if="!slot.product">
|
||||
<div class="text-sm font-bold text-slate-300 dark:text-slate-600 tracking-tight">{{ __('Empty') }}</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<!-- Stock Level -->
|
||||
<div class="flex items-baseline justify-center gap-1">
|
||||
<span class="text-xl font-black tracking-tighter leading-none" x-text="slot.stock"></span>
|
||||
<span class="text-xs font-black opacity-30">/</span>
|
||||
<span class="text-sm font-bold opacity-50" x-text="slot.max_stock || 10"></span>
|
||||
</div>
|
||||
|
||||
<!-- Expiry Date -->
|
||||
<div class="text-sm font-black tracking-tight leading-none opacity-80" x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<template x-if="inventorySlots.length === 0 && !inventoryLoading">
|
||||
<div class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="p-4 rounded-full bg-slate-50 dark:bg-slate-800/50 mb-4 border border-slate-100 dark:border-slate-800/50">
|
||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5">
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
|
||||
__('No slot data available') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div><!-- /Body -->
|
||||
|
||||
</div>
|
||||
</div><!-- /Sliding panel -->
|
||||
</div>
|
||||
</div><!-- /Inventory Offcanvas -->
|
||||
|
||||
@endsection
|
||||
@@ -166,7 +166,12 @@
|
||||
<a href="#{{ $api['slug'] }}" class="luxury-nav-link"
|
||||
:class="{ 'active': activeSection === '{{ $api['slug'] }}' }"
|
||||
@click="activeSection = '{{ $api['slug'] }}'">
|
||||
<span>{{ $api['name'] }}</span>
|
||||
<div class="flex items-center gap-2 overflow-hidden w-full">
|
||||
<span class="flex-shrink-0 text-[10px] w-10 text-center font-black px-1.5 py-0.5 rounded-md uppercase tracking-tighter {{ $api['method'] === 'GET' ? 'bg-emerald-50 text-emerald-600 border border-emerald-200' : ($api['method'] === 'POST' ? 'bg-cyan-50 text-cyan-600 border border-cyan-200' : 'bg-amber-50 text-amber-600 border border-amber-200') }}">
|
||||
{{ $api['method'] }}
|
||||
</span>
|
||||
<span class="truncate">{{ $api['name'] }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
@@ -187,7 +192,12 @@
|
||||
<div id="{{ $api['slug'] }}" class="mb-16 animate-luxury-in" x-intersect="activeSection = '{{ $api['slug'] }}'">
|
||||
<div class="mb-4"></div>
|
||||
|
||||
<h3 class="font-display text-3xl font-black text-slate-900 mb-6 tracking-tight">{{ $api['name'] }}</h3>
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<span class="px-3 py-1 text-sm font-black rounded-lg uppercase tracking-widest {{ $api['method'] === 'GET' ? 'bg-emerald-100 text-emerald-700' : ($api['method'] === 'POST' ? 'bg-cyan-100 text-cyan-700' : 'bg-amber-100 text-amber-700') }}">
|
||||
{{ $api['method'] }}
|
||||
</span>
|
||||
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $api['name'] }}</h3>
|
||||
</div>
|
||||
<p class="text-slate-600 mb-8 text-lg font-medium">{{ $api['description'] }}</p>
|
||||
|
||||
<!-- Headers & URL -->
|
||||
@@ -264,7 +274,7 @@
|
||||
<span
|
||||
class="text-[11px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
|
||||
<code
|
||||
class="text-sm text-cyan-600 font-bold">{{ is_array($param['example']) ? json_encode($param['example']) : $param['example'] }}</code>
|
||||
class="text-sm text-cyan-600 font-bold">{{ is_array($param['example']) ? json_encode($param['example'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $param['example'] }}</code>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@@ -280,7 +290,7 @@
|
||||
<!-- Request Example -->
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-6">請求範例 (Request Body)</h4>
|
||||
<pre><code>{{ json_encode($api['request'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</code></pre>
|
||||
<pre><code>{{ json_encode($api['request'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Response Examples -->
|
||||
@@ -310,7 +320,7 @@
|
||||
@if(isset($param['example']))
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
|
||||
<code class="text-xs text-cyan-600 font-bold bg-cyan-50/50 px-2 py-0.5 rounded">{{ $param['example'] }}</code>
|
||||
<code class="text-xs text-cyan-600 font-bold bg-cyan-50/50 px-2 py-0.5 rounded">{{ is_array($param['example']) ? json_encode($param['example'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $param['example'] }}</code>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@@ -322,7 +332,7 @@
|
||||
@endif
|
||||
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">回應範例 (Response Body)</h4>
|
||||
<pre><code>{{ json_encode($api['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</code></pre>
|
||||
<pre><code>{{ json_encode($api['response'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}</code></pre>
|
||||
</div>
|
||||
|
||||
@if(isset($api['notes']))
|
||||
|
||||
@@ -62,7 +62,7 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
|
||||
Route::post('machine/member/verify/B650', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyMember']);
|
||||
|
||||
// 廣告與貨道清單 (B005, B009)
|
||||
Route::post('machine/ad/B005', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getAdvertisements']);
|
||||
Route::get('machine/ad/B005', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getAdvertisements']);
|
||||
Route::put('products/supplementary/B009', [App\Http\Controllers\Api\V1\App\MachineController::class, 'reportSlotList']);
|
||||
|
||||
// 交易、發票與出貨 (B600, B601, B602)
|
||||
|
||||
Reference in New Issue
Block a user