2025-12-29 First Commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user