From 0a9f0b8583bb51b8d8bcd8360b47e3702d15eba8 Mon Sep 17 00:00:00 2001 From: "LAPTOP-EJ2PF6VA\\genie" Date: Sat, 3 Jan 2026 00:48:49 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E5=88=B0=E6=96=B0=E7=9B=AE=E9=8C=84(g?= =?UTF-8?q?ogs)=20=E4=B8=A6=E5=84=AA=E5=8C=96=20login=20=E5=8F=8A=E4=B8=8A?= =?UTF-8?q?=E5=82=B3=E5=9C=96=E7=89=87=E6=A9=9F=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/auth_manager.dart | 7 +++++- lib/expense/expense_api.dart | 4 +-- lib/expense/expense_detail.dart | 36 +++++++++++++++++++-------- lib/expense/expense_model.dart | 30 +++++++++++++++++++++- lib/login_page.dart | 29 ++++++++++++++++----- lib/services/generic_api_service.dart | 17 ++++++++++--- 6 files changed, 99 insertions(+), 24 deletions(-) diff --git a/lib/auth_manager.dart b/lib/auth_manager.dart index 79df809..0839d2b 100644 --- a/lib/auth_manager.dart +++ b/lib/auth_manager.dart @@ -9,28 +9,33 @@ class AuthManager { // 記憶體中的快取,讓您可以直接同步存取 String? currentUserId; String? currentToken; + String? currentCompany; // 記憶體快取 // 定義鍵值常數,避免硬編碼字串出錯 static const String _tokenKey = 'auth_token'; static const String _userIdKey = 'user_id'; // [新增] 用於儲存工號的鍵值 + static const String _companyKey = 'user_company'; // [新增] 公司別 Key /// 初始化:App 啟動時呼叫一次,把硬碟資料讀進記憶體 Future initialize() async { final prefs = await SharedPreferences.getInstance(); currentUserId = prefs.getString(_userIdKey); currentToken = prefs.getString(_tokenKey); + currentCompany = prefs.getString(_companyKey); // [新增] 初始化讀取 } /// 儲存登入資訊:包含 Token 與工號 /// 當登入成功時,同時呼叫此方法 - static Future saveLoginInfo(String token, String userId) async { + static Future saveLoginInfo(String token, String userId, String company) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_tokenKey, token); await prefs.setString(_userIdKey, userId); + await prefs.setString(_companyKey, company); // [新增] 儲存公司別 // 同步更新記憶體快取 AuthManager().currentUserId = userId; AuthManager().currentToken = token; + AuthManager().currentCompany = company; // [新增] 同步快取 } /// 讀取儲存的工號 (userid) diff --git a/lib/expense/expense_api.dart b/lib/expense/expense_api.dart index e2491ee..dbe866f 100644 --- a/lib/expense/expense_api.dart +++ b/lib/expense/expense_api.dart @@ -39,11 +39,11 @@ class ExpenseApiService { // 使用 GenericApiService 內定義的 BASE_IP var request = http.MultipartRequest( 'POST', - Uri.parse("$BASE_IP/upload/EIS_demo/images") + Uri.parse("${GenericApiService.BASE_IP}/upload/EIS_demo/images") ); // 取得本地檔案名稱 - String picFileName = "$BASE_IP/upload/EIS_demo/images/" + file.path.split('/').last; + String picFileName = "${GenericApiService.BASE_IP}/upload/EIS_demo/images/" + file.path.split('/').last; // [修正] index.js 的 multer 配置要求 key 必須是 'file' request.files.add(await http.MultipartFile.fromPath('file', file.path)); diff --git a/lib/expense/expense_detail.dart b/lib/expense/expense_detail.dart index 5415ba0..e71e20b 100644 --- a/lib/expense/expense_detail.dart +++ b/lib/expense/expense_detail.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import './expense_model.dart'; +import '../services/generic_api_service.dart'; class ExpenseDetail extends StatelessWidget { final ExpenseApply expense; @@ -170,17 +171,32 @@ class ExpenseDetail extends StatelessWidget { height: 250, width: double.infinity, fit: BoxFit.cover, + // [新增] 加載中的轉圈圈 + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 250, + color: Colors.grey.shade100, + child: const Center(child: CircularProgressIndicator()), + ); + }, // 處理圖片加載失敗 - errorBuilder: (context, error, stackTrace) => Container( - height: 200, - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.broken_image, color: Colors.grey, size: 40), - Text('無法載入單據圖片', style: TextStyle(color: Colors.grey)), - ], - ), - ), + errorBuilder: (context, error, stackTrace) { + print("圖片載入失敗,試圖存取的網址為: ${expense.fullPicUrl}"); + return Container( + height: 200, + color: Colors.grey.shade100, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.broken_image, color: Colors.red, size: 40), + const SizedBox(height: 8), + Text('無法載入圖片', style: TextStyle(color: Colors.grey.shade600)), + Text(expense.picPath ?? "無檔名", style: const TextStyle(fontSize: 10)), + ], + ), + ); + }, ), ), ), diff --git a/lib/expense/expense_model.dart b/lib/expense/expense_model.dart index 37f5e68..96884ad 100644 --- a/lib/expense/expense_model.dart +++ b/lib/expense/expense_model.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +// 在 expense_model.dart 中修改 +import '../auth_manager.dart'; +import '../services/generic_api_service.dart'; // --- 新增類別模型 --- class ExpenseClass { @@ -30,6 +33,31 @@ class ExpenseApply { final String? creatorId; final DateTime? createDateTime; + // [修改] 動態取得完整網址的 Getter + String get fullPicUrl { + if (picPath == null || picPath!.trim().isEmpty) return ""; + + // 1. 先去除前後空白,避免不可見字元的干擾 + String path = picPath!.trim(); + + // 2. 判斷是否已經是完整網址 (http/https) + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + + // 3. 【關鍵修正】檢查 path 開頭是否有斜線,如果有,把它去掉 + // 避免與前面的 "/images/" 組成 "//" 導致解析異常 + if (path.startsWith('/')) { + path = path.substring(1); + } + + // 4. 取得目前登入的公司別 + final String db = AuthManager().currentCompany ?? "demo"; + + // 5. 組合最終網址 + return "${GenericApiService.BASE_IP}/upload/eis_$db/images/$path"; + } + ExpenseApply({ this.applyId, required this.applyDate, @@ -59,5 +87,5 @@ class ExpenseApply { } // 輔助屬性:取得完整圖片路徑 (假設後端基礎 URL) - String get fullPicUrl => "https://api.gex.com.tw:8033/uploads/$picPath"; + // String get fullPicUrl => "https://api.gex.com.tw:8033/uploads/$picPath"; } \ No newline at end of file diff --git a/lib/login_page.dart b/lib/login_page.dart index 57529a3..9a6f548 100644 --- a/lib/login_page.dart +++ b/lib/login_page.dart @@ -8,8 +8,8 @@ import './auth_manager.dart'; // 確保路徑正確 // API 相關常數 const String BASE_IP = "https://api.gex.com.tw:8033"; -const String API_ENDPOINT = "xapi/v1/eis_demo/checklogin/2/"; -const String API_URL = "$BASE_IP/$API_ENDPOINT"; +//const String API_ENDPOINT = "xapi/v1/eis_demo/checklogin/2/"; +//const String API_URL = "$BASE_IP/$API_ENDPOINT"; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -22,6 +22,7 @@ class _LoginPageState extends State { // 用於獲取輸入框內容 final TextEditingController _userController = TextEditingController(text: '120102'); // 預設帳號 final TextEditingController _pwdController = TextEditingController(text: 'gex123'); // 預設密碼 + final TextEditingController _compController = TextEditingController(text: 'demo'); // [新增] 預設值 bool _isLoading = false; String? _errorMessage; @@ -33,7 +34,7 @@ class _LoginPageState extends State { // 登入邏輯,模擬您的 JS logon() 函數 Future _logon() async { - if (_userController.text.isEmpty || _pwdController.text.isEmpty) { + if (_userController.text.isEmpty || _pwdController.text.isEmpty || _compController.text.isEmpty) { setState(() { _errorMessage = '請輸入帳號和密碼'; }); @@ -60,8 +61,12 @@ class _LoginPageState extends State { try { // 執行 POST 請求。使用 application/x-www-form-urlencoded 格式 + // [關鍵] 動態組合登入 API 網址,取代固定的 eis_demo + final String comp = _compController.text.trim().toLowerCase(); + final String loginUrl = "$BASE_IP/xapi/v1/eis_$comp/checklogin/2/"; + final response = await http.post( - Uri.parse(API_URL), + Uri.parse(loginUrl), headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -82,7 +87,7 @@ class _LoginPageState extends State { : null; if (token != null && token.isNotEmpty) { - await AuthManager.saveLoginInfo(token, userId); // 呼叫 AuthManager 儲存 Token + await AuthManager.saveLoginInfo(token, userId, comp); // 呼叫 AuthManager 儲存 Token } else { print("警告: 登入成功但未收到 Token,將不儲存。"); } @@ -126,6 +131,18 @@ class _LoginPageState extends State { const Icon(Icons.lock_open, size: 80, color: Colors.blue), const SizedBox(height: 48.0), + // 公司別輸入框 + TextField( + controller: _compController, + decoration: const InputDecoration( + labelText: '公司代號', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person), + ), + keyboardType: TextInputType.text, + ), + const SizedBox(height: 16.0), + // 帳號輸入框 TextField( controller: _userController, @@ -149,7 +166,7 @@ class _LoginPageState extends State { ), onSubmitted: (_) => _logon(), // 允許按 Enter 鍵登入 ), - const SizedBox(height: 24.0), + const SizedBox(height: 16.0), // 錯誤訊息顯示 if (_errorMessage != null) diff --git a/lib/services/generic_api_service.dart b/lib/services/generic_api_service.dart index fcd966f..80ef561 100644 --- a/lib/services/generic_api_service.dart +++ b/lib/services/generic_api_service.dart @@ -3,11 +3,20 @@ import 'dart:convert'; import '../auth_manager.dart'; // 確保引入 AuthManager // 共用 API 常數 -const String BASE_IP = "https://api.gex.com.tw:8033"; -const String COMMON_API_ENDPOINT = "xapi/v2/eis_demo/orm_api_v2/2/"; -const String COMMON_API_URL = "$BASE_IP/$COMMON_API_ENDPOINT"; +//const String BASE_IP = "https://api.gex.com.tw:8033"; +//const String COMMON_API_ENDPOINT = "xapi/v2/eis_demo/orm_api_v2/2/"; +//const String COMMON_API_URL = "$BASE_IP/$COMMON_API_ENDPOINT"; class GenericApiService { + // [修改] 移除 const COMMON_API_URL,改為方法獲取 + static const String BASE_IP = "https://api.gex.com.tw:8033"; + + String _getDynamicUrl() { + // 從 AuthManager 獲取目前登入的公司別,若無則預設 eis_demo + final String db = AuthManager().currentCompany ?? "demo"; + final String db_all = "eis_" + db; + return "$BASE_IP/xapi/v2/$db_all/orm_api_v2/2/"; + } /// 通用的獲取列表方法 /// [T] : 目標資料模型 (例如 Announcement 或 Todo) @@ -49,7 +58,7 @@ class GenericApiService { try { final response = await http.post( - Uri.parse(COMMON_API_URL), + Uri.parse(_getDynamicUrl()), headers: { 'Content-Type': 'application/x-www-form-urlencoded', },