移到新目錄(gogs) 並優化 login 及上傳圖片機制
This commit is contained in:
@@ -9,28 +9,33 @@ class AuthManager {
|
|||||||
// 記憶體中的快取,讓您可以直接同步存取
|
// 記憶體中的快取,讓您可以直接同步存取
|
||||||
String? currentUserId;
|
String? currentUserId;
|
||||||
String? currentToken;
|
String? currentToken;
|
||||||
|
String? currentCompany; // 記憶體快取
|
||||||
|
|
||||||
// 定義鍵值常數,避免硬編碼字串出錯
|
// 定義鍵值常數,避免硬編碼字串出錯
|
||||||
static const String _tokenKey = 'auth_token';
|
static const String _tokenKey = 'auth_token';
|
||||||
static const String _userIdKey = 'user_id'; // [新增] 用於儲存工號的鍵值
|
static const String _userIdKey = 'user_id'; // [新增] 用於儲存工號的鍵值
|
||||||
|
static const String _companyKey = 'user_company'; // [新增] 公司別 Key
|
||||||
|
|
||||||
/// 初始化:App 啟動時呼叫一次,把硬碟資料讀進記憶體
|
/// 初始化:App 啟動時呼叫一次,把硬碟資料讀進記憶體
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
currentUserId = prefs.getString(_userIdKey);
|
currentUserId = prefs.getString(_userIdKey);
|
||||||
currentToken = prefs.getString(_tokenKey);
|
currentToken = prefs.getString(_tokenKey);
|
||||||
|
currentCompany = prefs.getString(_companyKey); // [新增] 初始化讀取
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 儲存登入資訊:包含 Token 與工號
|
/// 儲存登入資訊:包含 Token 與工號
|
||||||
/// 當登入成功時,同時呼叫此方法
|
/// 當登入成功時,同時呼叫此方法
|
||||||
static Future<void> saveLoginInfo(String token, String userId) async {
|
static Future<void> saveLoginInfo(String token, String userId, String company) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString(_tokenKey, token);
|
await prefs.setString(_tokenKey, token);
|
||||||
await prefs.setString(_userIdKey, userId);
|
await prefs.setString(_userIdKey, userId);
|
||||||
|
await prefs.setString(_companyKey, company); // [新增] 儲存公司別
|
||||||
|
|
||||||
// 同步更新記憶體快取
|
// 同步更新記憶體快取
|
||||||
AuthManager().currentUserId = userId;
|
AuthManager().currentUserId = userId;
|
||||||
AuthManager().currentToken = token;
|
AuthManager().currentToken = token;
|
||||||
|
AuthManager().currentCompany = company; // [新增] 同步快取
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 讀取儲存的工號 (userid)
|
/// 讀取儲存的工號 (userid)
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ class ExpenseApiService {
|
|||||||
// 使用 GenericApiService 內定義的 BASE_IP
|
// 使用 GenericApiService 內定義的 BASE_IP
|
||||||
var request = http.MultipartRequest(
|
var request = http.MultipartRequest(
|
||||||
'POST',
|
'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'
|
// [修正] index.js 的 multer 配置要求 key 必須是 'file'
|
||||||
request.files.add(await http.MultipartFile.fromPath('file', file.path));
|
request.files.add(await http.MultipartFile.fromPath('file', file.path));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import './expense_model.dart';
|
import './expense_model.dart';
|
||||||
|
import '../services/generic_api_service.dart';
|
||||||
|
|
||||||
class ExpenseDetail extends StatelessWidget {
|
class ExpenseDetail extends StatelessWidget {
|
||||||
final ExpenseApply expense;
|
final ExpenseApply expense;
|
||||||
@@ -170,17 +171,32 @@ class ExpenseDetail extends StatelessWidget {
|
|||||||
height: 250,
|
height: 250,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
fit: BoxFit.cover,
|
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(
|
errorBuilder: (context, error, stackTrace) {
|
||||||
height: 200,
|
print("圖片載入失敗,試圖存取的網址為: ${expense.fullPicUrl}");
|
||||||
child: const Column(
|
return Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
height: 200,
|
||||||
children: [
|
color: Colors.grey.shade100,
|
||||||
Icon(Icons.broken_image, color: Colors.grey, size: 40),
|
child: Column(
|
||||||
Text('無法載入單據圖片', style: TextStyle(color: Colors.grey)),
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
// 在 expense_model.dart 中修改
|
||||||
|
import '../auth_manager.dart';
|
||||||
|
import '../services/generic_api_service.dart';
|
||||||
|
|
||||||
// --- 新增類別模型 ---
|
// --- 新增類別模型 ---
|
||||||
class ExpenseClass {
|
class ExpenseClass {
|
||||||
@@ -30,6 +33,31 @@ class ExpenseApply {
|
|||||||
final String? creatorId;
|
final String? creatorId;
|
||||||
final DateTime? createDateTime;
|
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({
|
ExpenseApply({
|
||||||
this.applyId,
|
this.applyId,
|
||||||
required this.applyDate,
|
required this.applyDate,
|
||||||
@@ -59,5 +87,5 @@ class ExpenseApply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 輔助屬性:取得完整圖片路徑 (假設後端基礎 URL)
|
// 輔助屬性:取得完整圖片路徑 (假設後端基礎 URL)
|
||||||
String get fullPicUrl => "https://api.gex.com.tw:8033/uploads/$picPath";
|
// String get fullPicUrl => "https://api.gex.com.tw:8033/uploads/$picPath";
|
||||||
}
|
}
|
||||||
+23
-6
@@ -8,8 +8,8 @@ import './auth_manager.dart'; // 確保路徑正確
|
|||||||
|
|
||||||
// API 相關常數
|
// API 相關常數
|
||||||
const String BASE_IP = "https://api.gex.com.tw:8033";
|
const String BASE_IP = "https://api.gex.com.tw:8033";
|
||||||
const String API_ENDPOINT = "xapi/v1/eis_demo/checklogin/2/";
|
//const String API_ENDPOINT = "xapi/v1/eis_demo/checklogin/2/";
|
||||||
const String API_URL = "$BASE_IP/$API_ENDPOINT";
|
//const String API_URL = "$BASE_IP/$API_ENDPOINT";
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
@@ -22,6 +22,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
// 用於獲取輸入框內容
|
// 用於獲取輸入框內容
|
||||||
final TextEditingController _userController = TextEditingController(text: '120102'); // 預設帳號
|
final TextEditingController _userController = TextEditingController(text: '120102'); // 預設帳號
|
||||||
final TextEditingController _pwdController = TextEditingController(text: 'gex123'); // 預設密碼
|
final TextEditingController _pwdController = TextEditingController(text: 'gex123'); // 預設密碼
|
||||||
|
final TextEditingController _compController = TextEditingController(text: 'demo'); // [新增] 預設值
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
@@ -33,7 +34,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
// 登入邏輯,模擬您的 JS logon() 函數
|
// 登入邏輯,模擬您的 JS logon() 函數
|
||||||
Future<void> _logon() async {
|
Future<void> _logon() async {
|
||||||
if (_userController.text.isEmpty || _pwdController.text.isEmpty) {
|
if (_userController.text.isEmpty || _pwdController.text.isEmpty || _compController.text.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = '請輸入帳號和密碼';
|
_errorMessage = '請輸入帳號和密碼';
|
||||||
});
|
});
|
||||||
@@ -60,8 +61,12 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 執行 POST 請求。使用 application/x-www-form-urlencoded 格式
|
// 執行 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(
|
final response = await http.post(
|
||||||
Uri.parse(API_URL),
|
Uri.parse(loginUrl),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
@@ -82,7 +87,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
await AuthManager.saveLoginInfo(token, userId); // 呼叫 AuthManager 儲存 Token
|
await AuthManager.saveLoginInfo(token, userId, comp); // 呼叫 AuthManager 儲存 Token
|
||||||
} else {
|
} else {
|
||||||
print("警告: 登入成功但未收到 Token,將不儲存。");
|
print("警告: 登入成功但未收到 Token,將不儲存。");
|
||||||
}
|
}
|
||||||
@@ -126,6 +131,18 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
const Icon(Icons.lock_open, size: 80, color: Colors.blue),
|
const Icon(Icons.lock_open, size: 80, color: Colors.blue),
|
||||||
const SizedBox(height: 48.0),
|
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(
|
TextField(
|
||||||
controller: _userController,
|
controller: _userController,
|
||||||
@@ -149,7 +166,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
onSubmitted: (_) => _logon(), // 允許按 Enter 鍵登入
|
onSubmitted: (_) => _logon(), // 允許按 Enter 鍵登入
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24.0),
|
const SizedBox(height: 16.0),
|
||||||
|
|
||||||
// 錯誤訊息顯示
|
// 錯誤訊息顯示
|
||||||
if (_errorMessage != null)
|
if (_errorMessage != null)
|
||||||
|
|||||||
@@ -3,11 +3,20 @@ import 'dart:convert';
|
|||||||
import '../auth_manager.dart'; // 確保引入 AuthManager
|
import '../auth_manager.dart'; // 確保引入 AuthManager
|
||||||
|
|
||||||
// 共用 API 常數
|
// 共用 API 常數
|
||||||
const String BASE_IP = "https://api.gex.com.tw:8033";
|
//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_ENDPOINT = "xapi/v2/eis_demo/orm_api_v2/2/";
|
||||||
const String COMMON_API_URL = "$BASE_IP/$COMMON_API_ENDPOINT";
|
//const String COMMON_API_URL = "$BASE_IP/$COMMON_API_ENDPOINT";
|
||||||
|
|
||||||
class GenericApiService {
|
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)
|
/// [T] : 目標資料模型 (例如 Announcement 或 Todo)
|
||||||
@@ -49,7 +58,7 @@ class GenericApiService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse(COMMON_API_URL),
|
Uri.parse(_getDynamicUrl()),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user