修正上架時 Google Play 檢查出的 issue

This commit is contained in:
2026-04-09 23:15:19 +08:00
parent af27b77ad7
commit bdc0ee7dd4
108 changed files with 3446 additions and 276 deletions
+49
View File
@@ -0,0 +1,49 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'auth_manager.dart';
class AuthApiService {
static const String BASE_IP = "https://api.gex.com.tw";
// 輔助函數:將字串轉換為 MD5
String _generateMd5(String input) {
return md5.convert(utf8.encode(input)).toString();
}
/// 登入 API 呼叫
/// [comp] 公司代號
/// [userId] 帳號
/// [password] 原始密碼
Future<Map<String, dynamic>> login({
required String comp,
required String userId,
required String password,
}) async {
final String md5Password = _generateMd5(password);
final String loginUrl = "$BASE_IP/xapi/v1/eis_${comp.trim().toLowerCase()}/checklogin/2/";
final Map<String, String> params = {
'token': 'xxx',
'para01': userId,
'para02': md5Password,
'para03': 'web',
};
try {
final response = await http.post(
Uri.parse(loginUrl),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params,
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
return {'code': -1, 'msg': '伺服器響應錯誤: ${response.statusCode}'};
}
} catch (e) {
return {'code': -1, 'msg': '網路連線異常'};
}
}
}
+1 -1
View File
@@ -213,7 +213,7 @@ class _CalendarFormState extends State<CalendarForm> {
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: DropdownButtonFormField<String>(
value: value,
initialValue: value,
decoration: InputDecoration(labelText: label, border: InputBorder.none, labelStyle: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
items: items.map((s) => DropdownMenuItem(value: s, child: Text(displayNames != null ? displayNames[s]! : s, style: const TextStyle(fontSize: 15)))).toList(),
onChanged: onChanged,
+5 -1
View File
@@ -167,7 +167,11 @@ class _ChannelSalesManagerState extends State<ChannelSalesManager> {
String? picked = await _showMonthPicker(context, isStart ? _startYYMM : _endYYMM);
if (picked != null) {
setState(() {
if (isStart) _startYYMM = picked; else _endYYMM = picked;
if (isStart) {
_startYYMM = picked;
} else {
_endYYMM = picked;
}
});
}
},
+1 -1
View File
@@ -195,7 +195,7 @@ class _ClockInManagerState extends State<ClockInManager> {
padding: const EdgeInsets.all(12),
color: Colors.white,
child: DropdownButtonFormField<ClockInStore>(
value: _selectedStore,
initialValue: _selectedStore,
decoration: const InputDecoration(labelText: "目前打卡店點", border: OutlineInputBorder()),
items: _stores.map((s) => DropdownMenuItem(value: s, child: Text(s.storeName))).toList(),
onChanged: (val) {
+2 -2
View File
@@ -14,12 +14,12 @@ class PersonApiService {
filterPart = "personcname^%$searchName%";
}
String v_queryFilter = "1^100^personid^*^^^$filterPart";
String vQueryfilter = "1^100^personid^*^^^$filterPart";
return await _apiService.fetchList<Person>(
tableName: "basperson", // 對應到 basperson 表格
pk: "personid", // 主鍵為 personid
queryFilter: v_queryFilter,
queryFilter: vQueryfilter,
fromJson: (json) => Person.fromJson(json),
);
}
+30 -59
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
// 1. 必須加入這一行,否則系統不認識 launchUrl
import 'package:url_launcher/url_launcher.dart';
import './person_model.dart';
import 'package:url_launcher/url_launcher.dart'; // 用於撥打電話和發送郵件
class PersonDetail extends StatelessWidget {
final Person person;
@@ -18,10 +19,10 @@ class PersonDetail extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 姓名與職稱
CircleAvatar(
radius: 50,
backgroundColor: person.sexColor.withOpacity(0.2),
// 修正 .withOpacity 警告,改用新的 .withValues
backgroundColor: person.sexColor.withValues(alpha: 0.2),
child: Icon(person.sexIcon, size: 60, color: person.sexColor),
),
const SizedBox(height: 10),
@@ -35,48 +36,17 @@ class PersonDetail extends StatelessWidget {
),
const Divider(height: 30.0, thickness: 1),
// 聯絡資訊列表
_buildInfoTile(
Icons.badge,
'工號 / ID',
person.personId,
),
_buildInfoTile(
Icons.business,
'部門代號',
person.departmentId ?? 'N/A',
),
_buildActionTile(
context,
Icons.phone,
'公司電話',
person.tel ?? 'N/A',
person.tel,
'tel:',
),
_buildActionTile(
context,
Icons.smartphone,
'手機號碼',
person.cellphone ?? 'N/A',
person.cellphone,
'tel:',
),
_buildActionTile(
context,
Icons.email,
'電子郵件',
person.email ?? 'N/A',
person.email,
'mailto:',
),
_buildInfoTile(Icons.badge, '工號 / ID', person.personId),
_buildInfoTile(Icons.business, '部門代號', person.departmentId ?? 'N/A'),
_buildActionTile(context, Icons.phone, '公司電話', person.tel ?? 'N/A', person.tel, 'tel:'),
_buildActionTile(context, Icons.smartphone, '手機號碼', person.cellphone ?? 'N/A', person.cellphone, 'tel:'),
_buildActionTile(context, Icons.email, '電子郵件', person.email ?? 'N/A', person.email, 'mailto:'),
],
),
),
);
}
// 靜態資訊欄位
Widget _buildInfoTile(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
@@ -88,14 +58,8 @@ class PersonDetail extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
Text(
value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
],
),
),
@@ -104,19 +68,29 @@ class PersonDetail extends StatelessWidget {
);
}
// 可操作(點擊撥號/發送郵件)的欄位
Widget _buildActionTile(
BuildContext context, IconData icon, String label, String displayValue,
String? actionValue, String protocol) {
final canLaunch = actionValue != null && actionValue.isNotEmpty;
Future<void> _launchUrl() async {
// 修正點:在 StatelessWidget 中我們不使用 mounted,直接處理即可
Future<void> _handleLaunch() async {
if (canLaunch) {
final uri = Uri.parse('$protocol$actionValue');
if (!await launchUrl(uri)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('無法打開 $displayValue')),
);
final Uri uri = Uri.parse('$protocol$actionValue');
try {
if (!await launchUrl(uri)) {
if (context.mounted) { // 修正點:StatelessWidget 要用 context.mounted
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('無法打開 $displayValue')),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('執行錯誤: $e')),
);
}
}
}
}
@@ -124,7 +98,7 @@ class PersonDetail extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: InkWell(
onTap: canLaunch ? _launchUrl : null,
onTap: canLaunch ? _handleLaunch : null,
child: Row(
children: [
Icon(icon, color: canLaunch ? Colors.deepPurple : Colors.grey[700], size: 24),
@@ -133,10 +107,7 @@ class PersonDetail extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
Text(
displayValue,
style: TextStyle(
+2 -3
View File
@@ -1,11 +1,10 @@
import 'dart:io';
import 'dart:convert'; // [修正 1] 必須導入此包才能使用 json.decode
// [修正 1] 必須導入此包才能使用 json.decode
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../services/generic_api_service.dart';
import '../auth_manager.dart';
import 'expense_model.dart';
import 'package:path/path.dart' as p;
class ExpenseApiService {
final GenericApiService _apiService = GenericApiService();
@@ -44,7 +43,7 @@ class ExpenseApiService {
);
// 取得本地檔案名稱
String picFileName = "${GenericApiService.BASE_IP}/upload/eis_$db/images/" + file.path.split('/').last;
String picFileName = "${GenericApiService.BASE_IP}/upload/eis_$db/images/${file.path.split('/').last}";
// [修正] index.js 的 multer 配置要求 key 必須是 'file'
request.files.add(await http.MultipartFile.fromPath('file', file.path));
-1
View File
@@ -1,7 +1,6 @@
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;
+1 -1
View File
@@ -132,7 +132,7 @@ class _ExpenseFormState extends State<ExpenseForm> {
// [新增] 類別下拉選單
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedExpId,
initialValue: _selectedExpId,
decoration: const InputDecoration(labelText: "費用類別", border: OutlineInputBorder()),
items: _classList.map((c) => DropdownMenuItem(
value: c.expId,
-2
View File
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
// 在 expense_model.dart 中修改
import '../auth_manager.dart';
import '../services/generic_api_service.dart';
@@ -24,17 +24,17 @@ class PlaceholderScreen extends StatelessWidget {
// 各功能頁面 (繼承自 PlaceholderScreen,方便未來替換成真實頁面)
class AttendanceScreen extends PlaceholderScreen {
AttendanceScreen({super.key}) : super(title: '員工打卡');
const AttendanceScreen({super.key}) : super(title: '員工打卡');
}
class ApprovalScreen extends PlaceholderScreen {
ApprovalScreen({super.key}) : super(title: '待簽核事項');
const ApprovalScreen({super.key}) : super(title: '待簽核事項');
}
class TodoScreen extends PlaceholderScreen {
TodoScreen({super.key}) : super(title: '待辦事項');
const TodoScreen({super.key}) : super(title: '待辦事項');
}
class CalendarScreen extends PlaceholderScreen {
CalendarScreen({super.key}) : super(title: '行事曆');
const CalendarScreen({super.key}) : super(title: '行事曆');
}
+205
View File
@@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:crypto/crypto.dart';
// 引入九宮格主選單,登入成功後跳轉
import '../main.dart';
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";
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
// 用於獲取輸入框內容
final TextEditingController _userController = TextEditingController(text: '120102'); // 預設帳號
final TextEditingController _pwdController = TextEditingController(text: 'gex123'); // 預設密碼
final TextEditingController _compController = TextEditingController(text: 'demo'); // [新增] 預設值
bool _isLoading = false;
String? _errorMessage;
// 輔助函數:將字串轉換為 MD5 雜湊值
String _generateMd5(String input) {
return md5.convert(utf8.encode(input)).toString();
}
// 登入邏輯,模擬您的 JS logon() 函數
Future<void> _logon() async {
if (_userController.text.isEmpty || _pwdController.text.isEmpty || _compController.text.isEmpty) {
setState(() {
_errorMessage = '請輸入帳號和密碼';
});
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
final String userId = _userController.text;
final String password = _pwdController.text;
// 對密碼進行 MD5 加密
final String md5Password = _generateMd5(password);
// 建立 API 參數,使用您的邏輯
final Map<String, String> params = {
'token': 'xxx',
'para01': userId,
'para02': md5Password,
'para03': 'web',
};
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(loginUrl),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
);
// 解析 API 回應
final Map<String, dynamic> responseData = json.decode(response.body);
// 根據您的 API 邏輯:res.data.code === 0 為成功
if (responseData['code'] == 0) {
// 1. 預期 responseData['data'] 是一個 List
final List<dynamic>? dataList = responseData['data'] as List<dynamic>?;
// 2. 檢查 List 不為空,並從第一個元素中提取 'token'
final String? token = (dataList != null && dataList.isNotEmpty)
? (dataList[0] as Map<String, dynamic>)['token'] as String?
: null;
if (token != null && token.isNotEmpty) {
await AuthManager.saveLoginInfo(token, userId, comp); // 呼叫 AuthManager 儲存 Token
} else {
print("警告: 登入成功但未收到 Token,將不儲存。");
}
// 登入成功:導航至 MainMenu 並替換登入頁面(防止按返回鍵回到登入)
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainMenu()),
);
}
} else {
// 登入失敗:顯示錯誤訊息
setState(() {
_errorMessage = '登入失敗,請重新輸入正確的帳號及密碼!';
});
}
} catch (e) {
// 網路或伺服器錯誤
setState(() {
_errorMessage = '網路錯誤或伺服器無法連線。';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用戶登入')),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
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,
decoration: const InputDecoration(
labelText: '用戶帳號',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
keyboardType: TextInputType.text,
),
const SizedBox(height: 16.0),
// 密碼輸入框
TextField(
controller: _pwdController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密碼',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
onSubmitted: (_) => _logon(), // 允許按 Enter 鍵登入
),
const SizedBox(height: 16.0),
// 錯誤訊息顯示
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
// 登入按鈕
ElevatedButton(
onPressed: _isLoading ? null : _logon,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 3.0),
)
: const Text(
'登入',
style: TextStyle(fontSize: 18),
),
),
],
),
),
),
);
}
}
+2 -2
View File
@@ -12,8 +12,8 @@ class IssueApiService {
// 篩選條件:指派給當前使用者 (responsible = currentUserId) 且狀態非 '已結案' (Status != 4)
// 格式: 1^100^raised_date^*^responsible^=^$currentUserId^issue_status^!=^4
// 讀取 user_id
String? user_id = await AuthManager.getUserId();
String filterPart = "responsible^$user_id";
String? userId = await AuthManager.getUserId();
String filterPart = "responsible^$userId";
// 完整的 queryFilter 格式:Page^PageSize^SortColumn^SortOrder^Filter...
String queryFilter = "1^100^issueid^*^^^$filterPart";
+1 -1
View File
@@ -45,7 +45,7 @@ class Issue {
final descriptionLength = rawDescription?.length ?? 0;
final truncatedDescription = (descriptionLength > 50)
? rawDescription!.substring(0, 50) + '...' // 列表截斷
? '${rawDescription!.substring(0, 50)}...' // 列表截斷
: rawDescription;
return Issue(
+1 -1
View File
@@ -191,7 +191,7 @@ class LeaveApiService {
return await _apiService.fetchProcedure<Map<String, dynamic>>(
procedureEndpoint: "bpm_sign_history",
params: params,
fromJson: (json) => json as Map<String, dynamic>,
fromJson: (json) => json,
);
} catch (e) {
print("LeaveApiService.fetchSignHistory 異常: $e");
+1 -2
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import './leave_model.dart';
import './leave_api.dart';
@@ -242,7 +241,7 @@ class _LeaveDetailState extends State<LeaveDetail> {
_buildTableCell(displayTime),
],
);
}).toList(),
}),
],
),
);
+1 -1
View File
@@ -130,7 +130,7 @@ class _LeaveFormState extends State<LeaveForm> {
children: [
// 假別選擇 (顯示名稱,存入代碼)
DropdownButtonFormField<LeaveType>(
value: _selectedType,
initialValue: _selectedType,
decoration: const InputDecoration(labelText: '請假類別', border: OutlineInputBorder()),
// 將 API 取得的資料轉換為選單項目
items: _dbLeaveTypes.map((t) => DropdownMenuItem(
+61 -135
View File
@@ -1,15 +1,7 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:crypto/crypto.dart';
// 引入九宮格主選單,登入成功後跳轉
import 'main.dart';
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";
import 'auth_manager.dart';
import 'auth_api.dart'; // 引入新抽離的 API Service
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -19,25 +11,21 @@ class LoginPage extends StatefulWidget {
}
class _LoginPageState extends State<LoginPage> {
// 用於獲取輸入框內容
final TextEditingController _userController = TextEditingController(text: '120102'); // 預設帳號
final TextEditingController _pwdController = TextEditingController(text: 'gex123'); // 預設密碼
final TextEditingController _compController = TextEditingController(text: 'demo'); // [新增] 預設值
final TextEditingController _userController = TextEditingController(text: '120102');
final TextEditingController _pwdController = TextEditingController(text: 'gex123');
final TextEditingController _compController = TextEditingController(text: 'demo');
final AuthApiService _authApi = AuthApiService(); // 實例化 API 工具
bool _isLoading = false;
String? _errorMessage;
// 輔助函數:將字串轉換為 MD5 雜湊值
String _generateMd5(String input) {
return md5.convert(utf8.encode(input)).toString();
}
// 登入邏輯,模擬您的 JS logon() 函數
Future<void> _logon() async {
if (_userController.text.isEmpty || _pwdController.text.isEmpty || _compController.text.isEmpty) {
setState(() {
_errorMessage = '請輸入帳號和密碼';
});
final String comp = _compController.text.trim();
final String userId = _userController.text.trim();
final String password = _pwdController.text;
if (userId.isEmpty || password.isEmpty || comp.isEmpty) {
setState(() => _errorMessage = '請完整填寫公司、帳號與密碼');
return;
}
@@ -46,81 +34,48 @@ class _LoginPageState extends State<LoginPage> {
_errorMessage = null;
});
final String userId = _userController.text;
final String password = _pwdController.text;
// 對密碼進行 MD5 加密
final String md5Password = _generateMd5(password);
// 建立 API 參數,使用您的邏輯
final Map<String, String> params = {
'token': 'xxx',
'para01': userId,
'para02': md5Password,
'para03': 'web',
};
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(loginUrl),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
// 呼叫封裝好的 API Service
final responseData = await _authApi.login(
comp: comp,
userId: userId,
password: password,
);
// 解析 API 回應
final Map<String, dynamic> responseData = json.decode(response.body);
// 根據您的 API 邏輯:res.data.code === 0 為成功
if (responseData['code'] == 0) {
// 1. 預期 responseData['data'] 是一個 List
final List<dynamic>? dataList = responseData['data'] as List<dynamic>?;
// 2. 檢查 List 不為空,並從第一個元素中提取 'token'
final String? token = (dataList != null && dataList.isNotEmpty)
? (dataList[0] as Map<String, dynamic>)['token'] as String?
? dataList[0]['token'] as String?
: null;
if (token != null && token.isNotEmpty) {
await AuthManager.saveLoginInfo(token, userId, comp); // 呼叫 AuthManager 儲存 Token
} else {
print("警告: 登入成功但未收到 Token,將不儲存。");
}
if (token != null) {
// 儲存資訊至 AuthManager (包含公司別與 UserID)
await AuthManager.saveLoginInfo(token, userId, comp);
// 登入成功:導航至 MainMenu 並替換登入頁面(防止按返回鍵回到登入)
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainMenu()),
);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainMenu()),
);
}
} else {
setState(() => _errorMessage = '登入成功但未取得授權碼(Token)');
}
} else {
// 登入失敗:顯示錯誤訊息
setState(() {
_errorMessage = '登入失敗,請重新輸入正確的帳號及密碼!';
});
setState(() => _errorMessage = responseData['msg'] ?? '帳號或密碼錯誤');
}
} catch (e) {
// 網路或伺服器錯誤
setState(() {
_errorMessage = '網路錯誤或伺服器無法連線。';
});
setState(() => _errorMessage = '系統發生異常,請聯繫管理員');
} finally {
setState(() {
_isLoading = false;
});
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
// UI 部分保持不變,但按鈕觸發的是重構後的 _logon
return Scaffold(
appBar: AppBar(title: const Text('用戶登入')),
appBar: AppBar(title: const Text('企業行動化系統')),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32.0),
@@ -128,73 +83,32 @@ class _LoginPageState extends State<LoginPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const Icon(Icons.lock_open, size: 80, color: Colors.blue),
const SizedBox(height: 48.0),
const Icon(Icons.business_center, size: 80, color: Colors.blueAccent),
const SizedBox(height: 40),
// 公司別輸入框
TextField(
controller: _compController,
decoration: const InputDecoration(
labelText: '公司代號',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
keyboardType: TextInputType.text,
),
const SizedBox(height: 16.0),
_buildTextField(_compController, '公司代號', Icons.domain),
const SizedBox(height: 16),
_buildTextField(_userController, '用戶帳號', Icons.person),
const SizedBox(height: 16),
_buildTextField(_pwdController, '密碼', Icons.lock, obscure: true),
// 帳號輸入框
TextField(
controller: _userController,
decoration: const InputDecoration(
labelText: '用戶帳號',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
keyboardType: TextInputType.text,
),
const SizedBox(height: 16.0),
// 密碼輸入框
TextField(
controller: _pwdController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密碼',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
onSubmitted: (_) => _logon(), // 允許按 Enter 鍵登入
),
const SizedBox(height: 16.0),
// 錯誤訊息顯示
const SizedBox(height: 20),
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
_errorMessage!,
Text(_errorMessage!,
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
textAlign: TextAlign.center),
// 登入按鈕
const SizedBox(height: 20),
ElevatedButton(
onPressed: _isLoading ? null : _logon,
style: ElevatedButton.styleFrom(
// 修正點:使用 minimumSize 設定寬度與高度
// Size(double.infinity, 50) 代表寬度撐滿,高度為 50
minimumSize: const Size(double.infinity, 50),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 3.0),
)
: const Text(
'登入',
style: TextStyle(fontSize: 18),
),
? const CircularProgressIndicator()
: const Text('登入系統', style: TextStyle(fontSize: 18)),
),
],
),
@@ -202,4 +116,16 @@ class _LoginPageState extends State<LoginPage> {
),
);
}
Widget _buildTextField(TextEditingController controller, String label, IconData icon, {bool obscure = false}) {
return TextField(
controller: controller,
obscureText: obscure,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
border: const OutlineInputBorder(),
),
);
}
}
+5 -2
View File
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // 必須引入這個才能使用 kReleaseMode
import 'News/news_manager.dart';
import './placeholder_screens.dart';
import './login_page.dart'; // 確保引入登入頁面
import 'historys/login_page.dart'; // 確保引入登入頁面
import './auth_manager.dart';
/*
您的 Dart HTTP 客戶端可能無法完成與伺服器的 SSL/TLS 握手。這需要將您的整個 App 結構調整為使用 io.HttpClient
警告: 這是一個臨時且不安全的解決方案。 它會強制 Dart 客戶端信任所有憑證。只建議在無法控制伺服器憑證或開發環境中臨時使用。
@@ -30,7 +32,8 @@ class MyHttpOverrides extends HttpOverrides {
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true; // 總是回傳 true (忽略 SSL 錯誤)
(cert, host, port) => !kReleaseMode;
// (X509Certificate cert, String host, int port) => true; // 總是回傳 true (忽略 SSL 錯誤)
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ class MeetingDetail extends StatelessWidget {
dateRange = '$startDateStr$endDateStr';
}
final timeRange = (meeting.startTime ?? '') + ' ~ ' + (meeting.endTime ?? '');
final timeRange = '${meeting.startTime ?? ''} ~ ${meeting.endTime ?? ''}';
return '$dateRange ${timeRange.trim()}';
}
+4 -4
View File
@@ -24,17 +24,17 @@ class PlaceholderScreen extends StatelessWidget {
// 各功能頁面 (繼承自 PlaceholderScreen,方便未來替換成真實頁面)
class AttendanceScreen extends PlaceholderScreen {
AttendanceScreen({super.key}) : super(title: '員工打卡');
const AttendanceScreen({super.key}) : super(title: '員工打卡');
}
class ApprovalScreen extends PlaceholderScreen {
ApprovalScreen({super.key}) : super(title: '待簽核事項');
const ApprovalScreen({super.key}) : super(title: '待簽核事項');
}
class TodoScreen extends PlaceholderScreen {
TodoScreen({super.key}) : super(title: '待辦事項');
const TodoScreen({super.key}) : super(title: '待辦事項');
}
class CalendarScreen extends PlaceholderScreen {
CalendarScreen({super.key}) : super(title: '行事曆');
const CalendarScreen({super.key}) : super(title: '行事曆');
}
+3 -3
View File
@@ -9,7 +9,7 @@ import '../auth_manager.dart'; // 確保引入 AuthManager
class GenericApiService {
// [修改] 移除 const COMMON_API_URL,改為方法獲取
static const String BASE_IP = "https://api.gex.com.tw:8033";
static const String BASE_IP = "https://api.gex.com.tw"; // :8033
// 修改後的動態 URL 產生器,支援傳入不同的 store procedure 與回傳 dataset 數
String _getProcedureUrl(String endpoint, String version) {
@@ -75,8 +75,8 @@ class GenericApiService {
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/";
final String dbAll = "eis_$db";
return "$BASE_IP/xapi/v2/$dbAll/orm_api_v2/2/";
}
/// 通用的獲取列表方法
+1 -1
View File
@@ -74,7 +74,7 @@ class _PersonPickerDialogState extends State<PersonPickerDialog> {
: ListView.separated(
shrinkWrap: true,
itemCount: _results.length,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (ctx, i) {
final p = _results[i];
return ListTile(
+3 -2
View File
@@ -23,8 +23,9 @@ class TodoApiService {
// 2. 如果有傳入狀態過濾條件,動態加上 AND 語法
if (statusFilter != null && statusFilter != 'All') {
String dbStatus = '';
if (statusFilter == 'Done') dbStatus = 'DONE';
else if (statusFilter == 'In Progress') dbStatus = 'WIP';
if (statusFilter == 'Done') {
dbStatus = 'DONE';
} else if (statusFilter == 'In Progress') dbStatus = 'WIP';
else if (statusFilter == 'To do') dbStatus = 'TODO'; // 假設你的待辦狀態是 TODO
if (dbStatus.isNotEmpty) {
-1
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import './todo_model.dart';
import './todo_api.dart';
+1 -1
View File
@@ -144,7 +144,7 @@ class _TodoFormState extends State<TodoForm> {
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5))]),
child: DropdownButtonHideUnderline(
child: DropdownButtonFormField<String>(
value: value,
initialValue: value,
decoration: InputDecoration(labelText: label, labelStyle: const TextStyle(fontSize: 14, color: Colors.grey), border: InputBorder.none),
items: items.map((s) => DropdownMenuItem(value: s, child: Text(s, style: const TextStyle(fontWeight: FontWeight.bold)))).toList(),
onChanged: onChanged,