1. 實作「目前庫存」矩陣化視圖:將商品作為行、倉庫作為列,支援跨倉庫水位分析與自動加總。 2. 優化矩陣 UI:實作固定欄位 (Sticky Columns) 以應對多倉庫場景,並修正深色模式下的背景色對齊問題。 3. 強化倉儲操作驗證:實作調撥與補貨單的 Alpine.js 前端欄位驗證與 AJAX 提交邏輯,提升操作流暢度。 4. 優化機台地址定位:實作結構化地址解析與 Nominatim API 對接,提升經緯度自動獲取的準確性。 5. 標準化 UI 組件:更新 Breadcrumbs、刪除確認彈窗與搜尋下拉選單,確保全站視覺一致性。 6. 完善多語系支援:補全倉儲管理、機台設定與 UI 提示的繁體中文翻譯。 7. 增加 Product Model 的 stocks() 關聯,支援高效率的庫存矩陣查詢。
138 lines
4.4 KiB
PHP
138 lines
4.4 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Http;
|
||
|
||
/**
|
||
* 地址地理編碼代理控制器
|
||
*
|
||
* 透過後端轉發 Nominatim API 請求,解決瀏覽器無法自訂 User-Agent
|
||
* 導致 Nominatim 回傳 403 Forbidden 的問題。
|
||
*/
|
||
class GeocodingController extends Controller
|
||
{
|
||
/**
|
||
* 將台灣地址轉換為經緯度座標
|
||
*/
|
||
public function resolve(Request $request)
|
||
{
|
||
$request->validate([
|
||
'address' => 'required|string|max:255',
|
||
]);
|
||
|
||
$address = $request->input('address');
|
||
|
||
// 策略 1:結構化搜尋(台灣地址精確度最高)
|
||
$parsed = $this->parseTaiwanAddress($address);
|
||
|
||
if ($parsed['street']) {
|
||
$response = Http::withHeaders([
|
||
'User-Agent' => 'StarCloud/1.0 (admin@starcloud.com)',
|
||
])->get('https://nominatim.openstreetmap.org/search', [
|
||
'format' => 'json',
|
||
'street' => $parsed['street'],
|
||
'city' => $parsed['city'],
|
||
'country' => 'Taiwan',
|
||
'limit' => 1,
|
||
]);
|
||
|
||
if ($response->successful()) {
|
||
$data = $response->json();
|
||
if (!empty($data)) {
|
||
return response()->json([
|
||
'success' => true,
|
||
'lat' => (float) $data[0]['lat'],
|
||
'lon' => (float) $data[0]['lon'],
|
||
'display_name' => $data[0]['display_name'] ?? '',
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 策略 2:自由搜尋 + 台灣限定
|
||
$searchQuery = $address;
|
||
if (!str_contains($searchQuery, '台灣') && !str_contains($searchQuery, 'Taiwan')) {
|
||
$searchQuery .= ', 台灣';
|
||
}
|
||
|
||
$response = Http::withHeaders([
|
||
'User-Agent' => 'StarCloud/1.0 (admin@starcloud.com)',
|
||
])->get('https://nominatim.openstreetmap.org/search', [
|
||
'format' => 'json',
|
||
'q' => $searchQuery,
|
||
'countrycodes' => 'tw',
|
||
'limit' => 1,
|
||
]);
|
||
|
||
if ($response->successful()) {
|
||
$data = $response->json();
|
||
if (!empty($data)) {
|
||
return response()->json([
|
||
'success' => true,
|
||
'lat' => (float) $data[0]['lat'],
|
||
'lon' => (float) $data[0]['lon'],
|
||
'display_name' => $data[0]['display_name'] ?? '',
|
||
]);
|
||
}
|
||
}
|
||
|
||
return response()->json([
|
||
'success' => false,
|
||
'message' => __('Location not found'),
|
||
], 404);
|
||
}
|
||
|
||
/**
|
||
* 解析台灣地址格式為結構化參數
|
||
* 例如:「台中市北區興進路90之3號」→ city: "台中市 北區", street: "90之3 興進路"
|
||
*/
|
||
private function parseTaiwanAddress(string $address): array
|
||
{
|
||
$address = trim($address);
|
||
|
||
// 解析城市 (市/縣)
|
||
preg_match('/^(.+?[市縣])/u', $address, $cityMatch);
|
||
$city = $cityMatch[1] ?? '';
|
||
|
||
// 解析區域 (區/鄉/鎮) — 排除「市」以避免與城市混淆
|
||
preg_match('/[市縣](.+?[區鄉鎮])/u', $address, $districtMatch);
|
||
$district = $districtMatch[1] ?? '';
|
||
|
||
// 取得路街部分:從最後一個「區/鄉/鎮」之後開始截取
|
||
if ($district) {
|
||
// 精確定位到 district 結束後的部分
|
||
$pos = mb_strpos($address, $district);
|
||
if ($pos !== false) {
|
||
$streetPart = mb_substr($address, $pos + mb_strlen($district));
|
||
} else {
|
||
$streetPart = $address;
|
||
}
|
||
} else {
|
||
$streetPart = $address;
|
||
}
|
||
|
||
// 拆分路名與門牌號碼
|
||
preg_match('/^(.+?(?:路|街|大道|巷|弄)(?:[一二三四五六七八九十]+段)?)\s*(.*)/u', $streetPart, $roadMatch);
|
||
|
||
$street = '';
|
||
$houseNumber = '';
|
||
if ($roadMatch) {
|
||
$street = $roadMatch[1];
|
||
$houseNumber = str_replace('號', '', $roadMatch[2] ?? '');
|
||
} else {
|
||
$street = $streetPart;
|
||
}
|
||
|
||
$fullStreet = ($houseNumber ? $houseNumber . ' ' : '') . $street;
|
||
$fullCity = $city . ($district ? ' ' . $district : '');
|
||
|
||
return [
|
||
'city' => $fullCity,
|
||
'street' => $fullStreet,
|
||
];
|
||
}
|
||
}
|