修正上架時 Google Play 檢查出的 issue
This commit is contained in:
@@ -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': '網路連線異常'};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '行事曆');
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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(),
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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 錯誤)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}';
|
||||
}
|
||||
|
||||
|
||||
@@ -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: '行事曆');
|
||||
}
|
||||
@@ -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/";
|
||||
}
|
||||
|
||||
/// 通用的獲取列表方法
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import './todo_model.dart';
|
||||
import './todo_api.dart';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user