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
@@ -0,0 +1,18 @@
import './announcement_model.dart';
// 引入剛剛建立的共用服務 (請依實際路徑調整)
import '../services/generic_api_service.dart';
class AnnouncementApiService {
// 實例化共用服務
final GenericApiService _apiService = GenericApiService();
Future<List<Announcement>> fetchAnnouncements() async {
return await _apiService.fetchList<Announcement>(
tableName: "eipbbs_m",
pk: "uniqueno",
queryFilter: "1^10^uniqueno^*^^pmsm02^^",
// 將 Announcement 的轉換方法傳進去
fromJson: (json) => Announcement.fromJson(json),
);
}
}
@@ -0,0 +1,81 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
import './announcement_model.dart';
import '../auth_manager.dart'; // 確保引入 AuthManager
// API 相關常數 (假設與登入 API 使用相同的 Base URL 結構)
const String BASE_IP = "https://api.gex.com.tw:8033";
// **請替換成您實際獲取公告列表的 API 路徑**
const String ANNOUNCEMENT_LIST_ENDPOINT = "xapi/v2/eis_demo/orm_api/3/";
const String ANNOUNCEMENT_LIST_URL = "$BASE_IP/$ANNOUNCEMENT_LIST_ENDPOINT";
class AnnouncementApiService {
Future<List<Announcement>> fetchAnnouncements() async {
// 1. 讀取儲存的 Token
String? token = await AuthManager.getToken();
if (token == null) {
print("錯誤:未找到登入 Token,請重新登入!");
// 這裡通常會導航回登入頁面
// return;
}
// 建立 API 參數 (假設獲取公告列表需要類似的 token/身份驗證參數)
final Map<String, String> params = {
"token": '$token',
//"page": "${page}",
//"perPage": "${perPage}",
//"keywords": "${keywords}",
"_\$_tableName": "eipbbs_m",
"_\$_pk": "uniqueno",
"_\$_action": "P",
"_\$_query_filter": "1^10^uniqueno^*^^pmsm02^^"
};
try {
final response = await http.post(
Uri.parse(ANNOUNCEMENT_LIST_URL),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
);
// 檢查狀態碼
if (response.statusCode == 200) {
// 解析 API 回應
final Map<String, dynamic> responseData = json.decode(response.body);
// 1. 檢查 code 是否為 0
if (responseData['code'] == 0) {
// 2. 根據截圖,'data' 是一個 Map,裡面包著 'items'
final dynamic dataObject = responseData['data'];
// 檢查 dataObject 是否為 Map,且裡面是否有 'items' 並且是 List
if (dataObject is Map && dataObject['items'] is List) {
// 3. 取得真正的列表 'items'
List<dynamic> listJson = dataObject['items'];
// 將 JSON 列表轉換為 Announcement 物件列表
return listJson.map((json) => Announcement.fromJson(json)).toList();
} else {
// 雖然 code=0,但資料結構不符合預期 (例如 data 是 null 或沒有 items)
return []; // 或拋出異常,視您的需求而定
}
} else {
// API 邏輯失敗 (例如 code 不為 0)
throw Exception(responseData['msg'] ?? 'API 呼叫失敗,但連線成功。');
}
} else {
// HTTP 錯誤
throw Exception('HTTP 錯誤: ${response.statusCode}');
}
} catch (e) {
// 網路或解析錯誤
print('公告獲取錯誤: $e');
throw Exception('無法載入公告列表,請檢查網路。');
}
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import './announcement_model.dart';
// 引入 url_launcher 來處理附件 (可選,需要 pubspec.yaml 增加 url_launcher 套件)
// import 'package:url_launcher/url_launcher.dart';
class AnnouncementDetail extends StatelessWidget {
final Announcement announcement;
const AnnouncementDetail({required this.announcement, super.key});
// 處理附件下載的函數 (需要 url_launcher 套件)
/*
void _launchAttachment(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
// 處理錯誤
print('無法開啟附件連結: $url');
}
}
*/
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(announcement.title, overflow: TextOverflow.ellipsis),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 標題
Text(
announcement.title,
style: const TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 資訊列
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('發布日期: ${announcement.formattedBillDate}',
style: const TextStyle(color: Colors.grey, fontSize: 14)),
Text('作者: ${announcement.createdBy}',
style: const TextStyle(color: Colors.grey, fontSize: 14)),
],
),
const Divider(height: 32.0),
// 附件按鈕 (如果有)
if (announcement.attachment != null && announcement.attachment!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: TextButton.icon(
icon: const Icon(Icons.attach_file),
label: const Text('查看附件'),
onPressed: () {
// TODO: 實作附件下載/開啟邏輯
// _launchAttachment(announcement.attachment!);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('附件功能待實作。')),
);
},
),
),
// 公告詳細內容 (使用 Text 顯示)
Text(
announcement.description,
style: const TextStyle(
fontSize: 16.0,
height: 1.5,
),
textAlign: TextAlign.justify,
),
],
),
),
);
}
}
@@ -0,0 +1,167 @@
import 'package:flutter/material.dart';
import './announcement_api.dart';
import './announcement_model.dart';
import './announcement_detail.dart'; // 稍後創建
class AnnouncementManager extends StatefulWidget {
const AnnouncementManager({super.key});
@override
State<StatefulWidget> createState() {
return _AnnouncementManagerState();
}
}
class _AnnouncementManagerState extends State<AnnouncementManager> {
final AnnouncementApiService _apiService = AnnouncementApiService();
late Future<List<Announcement>> _announcementsFuture;
@override
void initState() {
super.initState();
// 頁面加載時自動開始獲取資料
_announcementsFuture = _apiService.fetchAnnouncements();
}
// 刷新資料的函數
void _refreshAnnouncements() {
setState(() {
_announcementsFuture = _apiService.fetchAnnouncements();
});
}
// 導航回主頁(與您 NewsManager 中的邏輯相似)
void _backToHome() {
// 假設 MainMenu 在 main.dart 中
// import './main.dart';
// Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (context) => const MainMenu()), (route) => false,);
// 在這裡我們假設它作為子頁面,使用 pop 即可返回
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('企業公告'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.refresh),
tooltip: '刷新列表',
onPressed: _refreshAnnouncements,
),
IconButton(
icon: const Icon(Icons.home),
tooltip: '返回主頁',
onPressed: _backToHome,
),
],
),
body: FutureBuilder<List<Announcement>>(
future: _announcementsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// 資料載入中
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
// 資料載入失敗 (顯示錯誤訊息)
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('載入失敗: ${snapshot.error}', textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(onPressed: _refreshAnnouncements, child: const Text('重試')),
],
),
);
} else if (snapshot.hasData && snapshot.data!.isNotEmpty) {
// 資料載入成功 (顯示列表)
return AnnouncementList(announcements: snapshot.data!);
} else {
// 沒有資料
return const Center(child: Text('目前沒有企業公告。'));
}
},
),
);
}
}
// -----------------------------------------------------------
// 列表顯示小部件 (取代原有的 News 類別)
// -----------------------------------------------------------
class AnnouncementList extends StatelessWidget {
final List<Announcement> announcements;
const AnnouncementList({required this.announcements, super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: announcements.length,
itemBuilder: (BuildContext context, int index) {
final item = announcements[index];
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: InkWell(
onTap: () {
// 點擊項目:導航到詳細頁面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AnnouncementDetail(announcement: item),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 公告標題
Text(
item.title,
style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// 發布日期和作者
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.calendar_today, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text(
item.formattedBillDate,
style: const TextStyle(fontSize: 14.0, color: Colors.grey),
),
],
),
Row(
children: [
const Icon(Icons.person, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text(
item.createdBy,
style: const TextStyle(fontSize: 14.0, color: Colors.grey),
),
],
),
],
),
],
),
),
),
);
},
);
}
}
@@ -0,0 +1,51 @@
import 'package:intl/intl.dart';
class Announcement {
final int uniqueNo;
final String title; // bbs_title
final String description; // bbs_desc (詳細內容)
final String createdBy; // create_user (作者)
final DateTime billDate; // billdate (發布日期)
final DateTime startDate; // startdate
final DateTime endDate; // end_date
final String? attachment; // bbs_attach
Announcement({
required this.uniqueNo,
required this.title,
required this.description,
required this.createdBy,
required this.billDate,
required this.startDate,
required this.endDate,
this.attachment,
});
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Announcement 物件
factory Announcement.fromJson(Map<String, dynamic> json) {
// 輔助函數:安全解析日期字串
DateTime? parseDate(dynamic date) {
if (date is String && date.isNotEmpty) {
// 假設日期格式為 YYYY-MM-DD HH:mm:ss.sss
return DateTime.tryParse(date);
}
return DateTime.now(); // 預設為當前時間或您可以選擇 null
}
return Announcement(
uniqueNo: json['uniqueno'] as int? ?? 0,
title: json['bbs_title'] as String? ?? '無標題',
description: json['bbs_desc'] as String? ?? '無詳細內容',
createdBy: json['create_user'] as String? ?? '未知作者',
billDate: parseDate(json['billdate'])!,
startDate: parseDate(json['startdate'])!,
endDate: parseDate(json['end_date'])!,
attachment: json['bbs_attach'] as String?,
);
}
// 格式化日期,用於列表顯示
String get formattedBillDate {
return DateFormat('yyyy/MM/dd HH:mm').format(billDate);
}
}