2025-12-29 First Commit

This commit is contained in:
DATAEXPRESS\4734
2025-12-29 15:20:40 +08:00
commit fb2603c6f0
92 changed files with 6006 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:crypto/crypto.dart';
// 引入九宮格主選單,登入成功後跳轉
import 'main.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: 'admin'); // 預設帳號
final TextEditingController _pwdController = TextEditingController(text: 'gex123'); // 預設密碼
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) {
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 格式
final response = await http.post(
Uri.parse(API_URL),
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) {
// 登入成功:導航至 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: _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: 24.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),
),
),
],
),
),
),
);
}
}
+232
View File
@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import './news_manager.dart';
import './placeholder_screens.dart';
import './login_page.dart'; // 確保引入登入頁面
/*
您的 Dart HTTP 客戶端可能無法完成與伺服器的 SSL/TLS 握手。這需要將您的整個 App 結構調整為使用 io.HttpClient
警告: 這是一個臨時且不安全的解決方案。 它會強制 Dart 客戶端信任所有憑證。只建議在無法控制伺服器憑證或開發環境中臨時使用。
還需要找時間查一下錯誤原因 20251214 modify by Gemini 3.0
*/
import 'dart:io';
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true; // 總是回傳 true (忽略 SSL 錯誤)
}
}
void main() {
// 在 main 函數中加入這行
HttpOverrides.global = MyHttpOverrides();
runApp(const ZenApp());
}
class ZenApp extends StatelessWidget {
const ZenApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '企業應用主選單',
theme: ThemeData(
primarySwatch: Colors.blue,
// 使用 Material 3 風格
useMaterial3: true,
),
// 設置主選單畫面為首頁
// home: const MainMenu(),
home: const LoginPage(),
);
}
}
// ------------------------------------
// 主選單的資料結構 (Data Model)
// ------------------------------------
class MenuItem {
final String title;
final IconData icon;
final Widget targetScreen;
const MenuItem({
required this.title,
required this.icon,
required this.targetScreen,
});
}
// 所有選單項目的列表
const List<MenuItem> menuItems = [
// 1. 新聞資訊 (使用您的 NewsManager)
MenuItem(
title: '公告事項',
icon: Icons.article,
targetScreen: NewsManager(),
),
// 2. 員工打卡
MenuItem(
title: '員工打卡',
icon: Icons.access_time_filled,
targetScreen: PlaceholderScreen(title: '員工打卡'),
),
// 2. 員工打卡
MenuItem(
title: '請假作業',
icon: Icons.access_time_filled,
targetScreen: PlaceholderScreen(title: '請假作業'),
), // 3. 待簽核事項
MenuItem(
title: '待簽核事項',
icon: Icons.pending_actions,
targetScreen: PlaceholderScreen(title: '待簽核事項'),
),
// 4. 待辦事項
MenuItem(
title: '待辦事項',
icon: Icons.checklist,
targetScreen: PlaceholderScreen(title: '待辦事項'),
),
// 5. 行事曆
MenuItem(
title: '行事曆',
icon: Icons.calendar_today,
targetScreen: PlaceholderScreen(title: '行事曆'),
),
// 6. 其他功能 (佔位)
MenuItem(
title: '會議通知',
icon: Icons.folder,
targetScreen: PlaceholderScreen(title: '會議通知'),
),
// 6. 其他功能 (佔位)
MenuItem(
title: '產品型錄',
icon: Icons.folder,
targetScreen: PlaceholderScreen(title: '產品型錄'),
),
// 6. 其他功能 (佔位)
MenuItem(
title: '通訊錄',
icon: Icons.folder,
targetScreen: PlaceholderScreen(title: '通訊錄'),
),
MenuItem(
title: '業績查詢',
icon: Icons.bar_chart,
targetScreen: PlaceholderScreen(title: '報告查詢'),
),
MenuItem(
title: 'Issue List',
icon: Icons.bar_chart,
targetScreen: PlaceholderScreen(title: 'Issue List'),
),
MenuItem(
title: '設定',
icon: Icons.settings,
targetScreen: PlaceholderScreen(title: '設定'),
),
];
// ------------------------------------
// 主選單畫面 (MainMenu) - 九宮格實作
// ------------------------------------
class MainMenu extends StatelessWidget {
const MainMenu({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('企業應用主選單'),
// 增加 App Bar 的高度,使其更符合 Material 3 風格 (可選)
toolbarHeight: 70,
// 可以在這裡加入登出按鈕
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
// 登出:導航回登入頁面並清除所有歷史堆棧
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
(Route<dynamic> route) => false,
);
},
),
],
),
// 使用 Padding 讓 GridView 內容與邊緣保持距離
body: Padding(
padding: const EdgeInsets.all(12.0),
// 核心:使用 GridView.count 建立九宮格佈局
child: GridView.count(
// 每行顯示 3 個項目(實現九宮格的第一條件)
crossAxisCount: 3,
// 項目間的間距
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
// 遍歷 menuItems 列表來構建每個選單項目
children: menuItems.map((item) {
return MainMenuItemTile(item: item);
}).toList(),
),
),
);
}
}
// ------------------------------------
// 單個選單項目的小部件 (MainMenuItemTile)
// ------------------------------------
class MainMenuItemTile extends StatelessWidget {
final MenuItem item;
const MainMenuItemTile({required this.item, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4.0, // 增加陰影,更有質感
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
child: InkWell(
// 使用 InkWell 獲得點擊時的漣漪效果
onTap: () {
// 點擊後導航到對應的目標畫面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => item.targetScreen,
),
);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 選單圖標
Icon(
item.icon,
size: 48.0,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 8.0),
// 選單名稱
Text(
item.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
// 1. 確保引入 news_detail.dart
import './news_detail.dart';
class News extends StatelessWidget {
final List<String> news;
// 構造函數保持不變
const News(this.news, {super.key});
@override
Widget build(BuildContext context) {
// 使用 ListView.builder 替代 Column 和 map().toList()
// 這樣可以確保內容可以捲動,並且只構建可見的項目,效率更高。
return ListView.builder(
// 設置列表的長度,即 news 列表的項目數
itemCount: news.length,
// 根據索引來構建每個列表項目
itemBuilder: (BuildContext context, int index) {
final String currentNewsTitle = news[index]; // 提取當前新聞標題
// 使用 news[index] 來取得對應的新聞文本
return Card(
elevation: 5, // 可選: 增加卡片陰影
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), // 增加邊距
// 在這裡可以選擇添加 Key
key: ValueKey(currentNewsTitle + index.toString()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // 讓圖片和文字可以寬度延伸
children: <Widget>[
// --- 點擊功能的修改在這裡 ---
GestureDetector(
onTap: () {
// 點擊事件:導航到 NewsDetail 畫面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewsDetail(
// 2. 將當前新聞的標題/內容傳遞給詳細頁面
newsTitle: currentNewsTitle,
),
),
);
},
child: Image.asset(
'assets/F16-02.jpg',
fit: BoxFit.cover,
height: 200,
),
),
// --------------------------
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
currentNewsTitle,
style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
),
),
],
),
);
},
);
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
// 這是一個 StatelessWidget,用於顯示單條新聞的詳細內容
class NewsDetail extends StatelessWidget {
final String newsTitle; // 接收從前一個畫面傳遞過來的新聞標題
// 為了演示,我們假設詳細內容是一個較長的文本
final String newsDetailContent =
"這是新聞的詳細內容。它包含了更深入的分析、更多的數據和完整的背景資訊。點擊圖片後,使用者可以在這個頁面仔細閱讀。由於這是一個範例,我們使用了一些重複的文本來模擬文章長度,以確保頁面可以滾動。在這個頁面,您可以加入更多的元素,例如不同的圖片、作者資訊、發布日期等。";
const NewsDetail({required this.newsTitle, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// 設置 App Bar,標題可以是傳入的新聞標題
appBar: AppBar(
title: Text(newsTitle, overflow: TextOverflow.ellipsis), // 防止標題過長
),
// 使用 SingleChildScrollView 確保頁面內容超出螢幕時可以滾動
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 再次顯示圖片 (可選)
ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.asset(
'assets/F16-02.jpg',
fit: BoxFit.cover,
height: 250,
width: double.infinity, // 佔滿寬度
),
),
const SizedBox(height: 16.0),
// 顯示新聞標題
Text(
newsTitle,
style: const TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.w800,
),
),
const Divider(height: 32.0), // 分隔線
// 顯示新聞詳細內容
Text(
newsDetailContent * 5, // 重複內容 5 次來模擬長文章
style: const TextStyle(
fontSize: 16.0,
height: 1.5, // 增加行高,改善閱讀體驗
),
textAlign: TextAlign.justify, // 內容兩端對齊
),
],
),
),
),
);
}
}
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
// 確保你的 news.dart 文件和 News 類已經被正確引入
import './news.dart';
// 確保引入 MainMenu 所在的 main.dart 文件,以便導航
import './main.dart';
class NewsManager extends StatefulWidget {
const NewsManager({super.key});
@override
State<StatefulWidget> createState() {
return _NewsManagerState();
}
}
class _NewsManagerState extends State<NewsManager> {
List<String> news = ['第一筆資料: F-16 戰機升級計畫', '第二筆資料: 台積電研發新突破'];
void _addNews() {
setState(() {
news.add('新增資料:最新動態 ${DateTime.now().second}');
});
}
// 新增的導航函數:返回到主選單
void _backToHome() {
// Navigator.popUntil 會一直彈出 (Pop) 路由堆棧中的頁面,直到遇到一個
// 滿足條件 (predicte) 的路由。在這裡,我們使用 (route) => route.isFirst
// 來返回到路由堆棧中的第一個頁面,也就是 MainMenu (如果您的啟動順序是 Login -> MainMenu)。
// 如果 MainMenu 不是第一個頁面,您可以考慮使用:
// Navigator.popUntil(context, ModalRoute.withName('/main_menu'));
// 但這需要您在 MaterialApp 中為 MainMenu 設定路由名稱。
// 最保險且簡單的方法是直接彈出所有,然後重新推入 MainMenu(確保 MainMenu 不會有歷史記錄)
// 或者我們假設 MainMenu 是 Login 成功後的第一個頁面,使用 pushReplacement 清理過的堆棧。
// 在本例中,我們直接導航回 MainMenu,並清除所有在它之上的頁面。
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const MainMenu()),
(Route<dynamic> route) => false, // 移除所有前面的路由
);
// 註:若 NewsManager 已經是 Scaffold 的子頁面,使用 Navigator.pop(context)
// 即可返回上一個頁面(即 MainMenu)。但如果想確保是回主頁,用 pushAndRemoveUntil 更明確。
}
@override
Widget build(BuildContext context) {
// *** 關鍵修改:將整個頁面包裹在 Scaffold 中以包含 AppBar ***
return Scaffold(
appBar: AppBar(
title: const Text('新聞管理'),
// ----------------------------------------------------
// 新增的返回主頁按鈕
actions: <Widget>[
IconButton(
icon: const Icon(Icons.home), // 使用一個 Home 圖標
tooltip: '返回主頁', // 長按時的提示
onPressed: _backToHome, // 點擊時呼叫導航函數
),
],
// ----------------------------------------------------
),
body: Column(
children: <Widget>[
// 1. 固定高度的按鈕部分
Container(
margin: const EdgeInsets.all(10),
child: ElevatedButton(
onPressed: _addNews,
child: const Text(
'新增資料',
style: TextStyle(fontSize: 20, color: Colors.redAccent),
),
),
),
// 2. 佔據剩餘空間的列表部分
Expanded(
child: News(news),
),
],
),
);
}
}
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
// 通用的佔位畫面
class PlaceholderScreen extends StatelessWidget {
final String title;
const PlaceholderScreen({required this.title, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Text(
'$title 功能正在開發中...',
style: const TextStyle(fontSize: 24, color: Colors.grey),
),
),
);
}
}
// 各功能頁面 (繼承自 PlaceholderScreen,方便未來替換成真實頁面)
class AttendanceScreen extends PlaceholderScreen {
AttendanceScreen({super.key}) : super(title: '員工打卡');
}
class ApprovalScreen extends PlaceholderScreen {
ApprovalScreen({super.key}) : super(title: '待簽核事項');
}
class TodoScreen extends PlaceholderScreen {
TodoScreen({super.key}) : super(title: '待辦事項');
}
class CalendarScreen extends PlaceholderScreen {
CalendarScreen({super.key}) : super(title: '行事曆');
}