修改幾個小 issue

This commit is contained in:
2026-03-21 21:23:56 +08:00
parent 9068d85c92
commit af27b77ad7
11 changed files with 674 additions and 120 deletions
+40 -15
View File
@@ -1,3 +1,5 @@
// calendar_api.dart
import './calendar_model.dart';
import '../services/generic_api_service.dart';
import 'package:intl/intl.dart';
@@ -5,13 +7,19 @@ import 'package:intl/intl.dart';
class CalendarApiService {
final GenericApiService _apiService = GenericApiService();
// 獲取指定月份或範圍的行程
Future<List<CalendarEvent>> fetchEvents(String userId, DateTime month) async {
// 企業實作建議:僅抓取特定月份資料,減少手機負載
final sDate = DateTime(month.year, month.month, 1);
final eDate = DateTime(month.year, month.month + 1, 0);
// 獲取指定「日期區間」的行程
Future<List<CalendarEvent>> fetchEvents(String userId, DateTime startDate, DateTime endDate) async {
// 格式化查詢的起迄日期
final sDate = DateFormat('yyyy-MM-dd').format(startDate);
// 結束日期加 1 天,用小於 (<) 來涵蓋 endDate 當天的所有時間 (23:59:59)
final eDate = DateFormat('yyyy-MM-dd').format(endDate.add(const Duration(days: 1)));
String queryFilter = "1^500^start_time^*^personid^=^$userId";
// 1. 組合 wheresql_org
// 條件:大於等於開始日期,小於結束日期的隔天,且人員代號相符
String whereSql = "start_time >= '$sDate' AND start_time < '$eDate' AND personid = '$userId'";
// 2. 嚴格依照 API 規範組裝 10 個參數
String queryFilter = "1^500^start_time^*^$whereSql^^^^^";
return await _apiService.fetchList<CalendarEvent>(
tableName: "eip_new_calendar",
@@ -21,19 +29,36 @@ class CalendarApiService {
);
}
// 新增/更新行程
Future<bool> saveEvent(CalendarEvent event) async {
final data = {
"uuid": event.uuid,
// 新增行程至資料庫
Future<bool> createEvent(CalendarEvent event) async {
final Map<String, dynamic> data = {
// uuid 通常由後端資料庫生成,新增時可以不傳或傳空值
"title": event.title,
"cal_description": event.description,
"start_time": event.startTime?.toIso8601String(),
"end_time": event.endTime?.toIso8601String(),
"start_time": event.startTime != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(event.startTime!) : null,
"end_time": event.endTime != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(event.endTime!) : null,
"cal_type": event.type,
"cal_level": event.level,
"personid": event.personId,
"cal_finished": event.isFinished ? 'Y' : 'N',
"update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
"projectid": event.projectId, // 關聯專案
"create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
};
// return await _apiService.saveData("eip_new_calendar", data);
return true;
try {
// 根據 Generic API 規範,使用 action: "C" 進行新增
await _apiService.fetchList<dynamic>(
tableName: "eip_new_calendar",
pk: "uuid",
queryFilter: "",
action: "C",
data: data,
fromJson: (json) => json,
);
return true;
} catch (e) {
print("Create Calendar Event Error: $e");
return false;
}
}
}
+252
View File
@@ -0,0 +1,252 @@
// calendar_form.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import './calendar_model.dart';
import './calendar_api.dart';
class CalendarForm extends StatefulWidget {
final String userId;
final DateTime? initialDate; // 接收從行事曆首頁傳來的預設日期
const CalendarForm({required this.userId, this.initialDate, super.key});
@override
State<CalendarForm> createState() => _CalendarFormState();
}
class _CalendarFormState extends State<CalendarForm> {
final _formKey = GlobalKey<FormState>();
final CalendarApiService _apiService = CalendarApiService();
final TextEditingController _titleController = TextEditingController();
final TextEditingController _descController = TextEditingController();
final TextEditingController _projectController = TextEditingController();
String _selectedType = '會議';
String _selectedLevel = 'NORMAL';
late DateTime _startDate;
late TimeOfDay _startTime;
late DateTime _endDate;
late TimeOfDay _endTime;
bool _isSubmitting = false;
@override
void initState() {
super.initState();
// 如果有傳入初始日期,就用它;否則用今天
DateTime baseDate = widget.initialDate ?? DateTime.now();
_startDate = baseDate;
_endDate = baseDate;
// 預設時間:目前時間的下一個整點,例如現在 10:25,預設 11:00 ~ 12:00
int nextHour = TimeOfDay.now().hour + 1;
_startTime = TimeOfDay(hour: nextHour > 23 ? 23 : nextHour, minute: 0);
_endTime = TimeOfDay(hour: nextHour + 1 > 23 ? 23 : nextHour + 1, minute: 0);
}
// 結合日期與時間的選擇器
Future<void> _pickDateTime(bool isStart) async {
final date = await showDatePicker(
context: context,
initialDate: isStart ? _startDate : _endDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
builder: (context, child) => Theme(
data: ThemeData.light().copyWith(colorScheme: const ColorScheme.light(primary: Colors.blueAccent)),
child: child!,
),
);
if (date == null) return;
final time = await showTimePicker(
context: context,
initialTime: isStart ? _startTime : _endTime,
builder: (context, child) => Theme(
data: ThemeData.light().copyWith(colorScheme: const ColorScheme.light(primary: Colors.blueAccent)),
child: child!,
),
);
if (time == null) return;
setState(() {
if (isStart) {
_startDate = date; _startTime = time;
// 防呆:如果結束日期早於開始日期,自動順延
if (_endDate.isBefore(_startDate)) _endDate = _startDate;
} else {
_endDate = date; _endTime = time;
}
});
}
void _handleSubmit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isSubmitting = true);
// 合併 Date 與 Time 成為完整的 DateTime
final start = DateTime(_startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute);
final end = DateTime(_endDate.year, _endDate.month, _endDate.day, _endTime.hour, _endTime.minute);
final newEvent = CalendarEvent(
uuid: '', // 由後端生成
title: _titleController.text,
description: _descController.text,
startTime: start,
endTime: end,
type: _selectedType,
level: _selectedLevel,
personId: widget.userId,
projectId: _projectController.text.isNotEmpty ? _projectController.text : null,
);
bool success = await _apiService.createEvent(newEvent);
if (mounted) {
setState(() => _isSubmitting = false);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('行程新增成功!')));
Navigator.pop(context, true); // 回傳 true 通知列表刷新
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('新增失敗,請檢查網路連線。', style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: const Text('新增行程', style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0.5,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextField('行程標題 *', _titleController, '例如:部門月會', Icons.title, isRequired: true),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildDropdownField('類型', _selectedType, ['會議', '拜訪客戶', '專案開發', '私人行程', '其他'], (v) => setState(() => _selectedType = v!))),
const SizedBox(width: 16),
Expanded(
child: _buildDropdownField('重要性', _selectedLevel,
['URGENT', 'HIGH', 'NORMAL'],
(v) => setState(() => _selectedLevel = v!),
displayNames: {'URGENT': '緊急', 'HIGH': '', 'NORMAL': '一般'}
)
),
],
),
const SizedBox(height: 24),
const Text('時間設定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey)),
const SizedBox(height: 12),
_buildTimePickerTile('開始時間', _startDate, _startTime, true),
const SizedBox(height: 12),
_buildTimePickerTile('結束時間', _endDate, _endTime, false),
const SizedBox(height: 24),
_buildTextField('專案關聯 (選填)', _projectController, '輸入專案代號或名稱', Icons.folder_open),
const SizedBox(height: 16),
_buildTextField('行程詳細說明', _descController, '記錄會議大綱或行程備註...', Icons.notes, isMultiline: true),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _handleSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: _isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: const Text('儲存行程', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
),
),
const SizedBox(height: 20),
],
),
),
),
);
}
// 共用輸入框元件
Widget _buildTextField(String label, TextEditingController controller, String hint, IconData icon, {bool isMultiline = false, bool isRequired = false}) {
return Container(
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: TextFormField(
controller: controller,
maxLines: isMultiline ? 4 : 1,
decoration: InputDecoration(
icon: Icon(icon, color: Colors.grey.shade400, size: 20),
labelText: label,
labelStyle: TextStyle(color: Colors.grey.shade600, fontSize: 14),
border: InputBorder.none,
hintText: hint,
hintStyle: TextStyle(color: Colors.grey.shade300),
),
validator: isRequired ? (v) => v == null || v.isEmpty ? '此欄位為必填' : null : null,
),
);
}
// 共用下拉選單元件 (支援實際值與顯示名稱不同)
Widget _buildDropdownField(String label, String value, List<String> items, ValueChanged<String?> onChanged, {Map<String, String>? displayNames}) {
return Container(
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,
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,
icon: Icon(Icons.keyboard_arrow_down, color: Colors.grey.shade400),
),
);
}
// 時間選擇按鈕元件
Widget _buildTimePickerTile(String label, DateTime date, TimeOfDay time, bool isStart) {
return InkWell(
onTap: () => _pickDateTime(isStart),
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
child: Row(
children: [
Icon(Icons.access_time, color: isStart ? Colors.blueAccent : Colors.orangeAccent, size: 22),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
const SizedBox(height: 4),
Text("${DateFormat('yyyy/MM/dd').format(date)} ${time.format(context)}", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
const Spacer(),
Icon(Icons.edit_calendar, color: Colors.grey.shade300, size: 20),
],
),
),
);
}
}
+299 -69
View File
@@ -1,9 +1,12 @@
// calendar_manager.dart
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart'; // 建議引入此套件
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart';
import './calendar_model.dart';
import './calendar_api.dart';
import './calendar_detail.dart';
import './calendar_form.dart';
class CalendarManager extends StatefulWidget {
final String userId;
@@ -16,33 +19,47 @@ class CalendarManager extends StatefulWidget {
class _CalendarManagerState extends State<CalendarManager> {
final CalendarApiService _apiService = CalendarApiService();
// 狀態控制
// ==== 日曆模式狀態 ====
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Map<DateTime, List<CalendarEvent>> _eventsMap = {}; // 供日曆標點使用
// 資料儲存:將事件按日期分類,方便日曆標點
Map<DateTime, List<CalendarEvent>> _eventsMap = {};
// ==== 列表模式狀態 ====
late DateTime _listStartDate;
late DateTime _listEndDate;
List<CalendarEvent> _listEvents = []; // 供列表模式獨立顯示使用
// 控制目前是日曆還是列表
String _viewMode = '日曆模式';
bool _isLoading = false;
@override
void initState() {
super.initState();
_selectedDay = _focusedDay;
// 初始化列表模式的預設區間為「本月 1 日」到「本月底」
_listStartDate = DateTime(DateTime.now().year, DateTime.now().month, 1);
_listEndDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0);
// 預設載入日曆資料
_fetchMonthEvents(_focusedDay);
}
// 核心:抓取整個月的資料並進行分類
// ============== API 抓取邏輯 ==============
// 1. 日曆模式:抓取整個月並轉換為 Map 標點
Future<void> _fetchMonthEvents(DateTime month) async {
setState(() => _isLoading = true);
try {
final events = await _apiService.fetchEvents(widget.userId, month);
final sDate = DateTime(month.year, month.month, 1);
final eDate = DateTime(month.year, month.month + 1, 0); // 當月最後一天
final events = await _apiService.fetchEvents(widget.userId, sDate, eDate);
// 將 List 轉換為 Map<DateTime, List<Event>>
Map<DateTime, List<CalendarEvent>> newMap = {};
for (var event in events) {
if (event.startTime != null) {
// 只取日期部分作為 Key
final dateKey = DateTime(event.startTime!.year, event.startTime!.month, event.startTime!.day);
if (newMap[dateKey] == null) newMap[dateKey] = [];
newMap[dateKey]!.add(event);
@@ -59,7 +76,65 @@ class _CalendarManagerState extends State<CalendarManager> {
}
}
// 獲取選定日期的行程
// 2. 列表模式:抓取特定日期區間,單純存成 List
Future<void> _fetchListEvents() async {
setState(() => _isLoading = true);
try {
final events = await _apiService.fetchEvents(widget.userId, _listStartDate, _listEndDate);
// 依開始時間排序
events.sort((a, b) => (a.startTime ?? DateTime.now()).compareTo(b.startTime ?? DateTime.now()));
setState(() {
_listEvents = events;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("列表載入失敗: $e")));
}
}
// 簡單式:獨立選擇開始或結束日期
Future<void> _selectDate(bool isStart) async {
final DateTime initialDate = isStart ? _listStartDate : _listEndDate;
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(primary: Colors.blueAccent),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
if (isStart) {
_listStartDate = picked;
// 防呆:如果開始日期大於結束日期,讓結束日期自動等於開始日期
if (_listStartDate.isAfter(_listEndDate)) {
_listEndDate = _listStartDate;
}
} else {
_listEndDate = picked;
// 防呆:如果結束日期小於開始日期,讓開始日期自動等於結束日期
if (_listEndDate.isBefore(_listStartDate)) {
_listStartDate = _listEndDate;
}
}
});
_fetchListEvents(); // 選定後立刻觸發 API 重新查詢
}
}
// 獲取日曆選定日期的行程
List<CalendarEvent> _getEventsForDay(DateTime day) {
final dateKey = DateTime(day.year, day.month, day.day);
return _eventsMap[dateKey] ?? [];
@@ -68,102 +143,257 @@ class _CalendarManagerState extends State<CalendarManager> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: const Text('企業行事曆'),
title: const Text('行事曆', style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0.5,
actions: [
IconButton(icon: const Icon(Icons.today), onPressed: () => setState(() => _focusedDay = DateTime.now())),
// 視圖切換下拉選單
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _viewMode,
icon: const Icon(Icons.arrow_drop_down, color: Colors.blueAccent),
items: ['日曆模式', '列表模式'].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
);
}).toList(),
onChanged: (newValue) {
if (newValue != null && newValue != _viewMode) {
setState(() => _viewMode = newValue);
// 切換模式時,呼叫對應的 API
if (newValue == '列表模式') {
_fetchListEvents();
} else {
_fetchMonthEvents(_focusedDay);
}
}
},
),
),
),
IconButton(
icon: const Icon(Icons.today, color: Colors.blueAccent),
onPressed: () {
setState(() {
_focusedDay = DateTime.now();
_selectedDay = DateTime.now();
// 如果是列表模式,也順便重置回當月區間
_listStartDate = DateTime(DateTime.now().year, DateTime.now().month, 1);
_listEndDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0);
});
_viewMode == '日曆模式'
? _fetchMonthEvents(_focusedDay)
: _fetchListEvents();
}
),
],
),
body: Column(
children: [
// 1. 日曆組件
TableCalendar<CalendarEvent>(
body: _viewMode == '日曆模式' ? _buildCalendarLayout() : _buildListLayout(),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.blueAccent,
child: const Icon(Icons.add, color: Colors.white),
onPressed: () async {
// 跳轉至新增表單,並將目前選擇的日期傳入作為預設值
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CalendarForm(
userId: widget.userId,
initialDate: _selectedDay, // 貼心設計:自動帶入你在日曆上點擊的日期
),
),
);
// 如果表單回傳 true (代表新增成功),就重新 Call API 抓取最新資料!
if (result == true) {
if (_viewMode == '日曆模式') {
_fetchMonthEvents(_focusedDay);
} else {
_fetchListEvents();
}
}
},
),
);
}
// ============== 視圖 1:日曆模式 ==============
Widget _buildCalendarLayout() {
return Column(
children: [
Container(
color: Colors.white,
child: TableCalendar<CalendarEvent>(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
eventLoader: _getEventsForDay, // 在有行程的日期顯示小點點
// 樣式設定
eventLoader: _getEventsForDay,
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.5), shape: BoxShape.circle),
selectedDecoration: const BoxDecoration(color: Colors.blue, shape: BoxShape.circle),
todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.3), shape: BoxShape.circle),
selectedDecoration: const BoxDecoration(color: Colors.blueAccent, shape: BoxShape.circle),
markerDecoration: const BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
),
// 互動事件
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() => _calendarFormat = format);
},
onFormatChanged: (format) => setState(() => _calendarFormat = format),
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
_fetchMonthEvents(focusedDay); // 切換月份時自動重新抓取 API
_fetchMonthEvents(focusedDay);
},
),
const Divider(height: 1),
// 2. 下方行程列表
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _buildEventList(),
),
],
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () { /* 實重新增行程邏輯 */ },
),
),
const SizedBox(height: 8),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _buildEventListForDay(),
),
],
);
}
Widget _buildEventList() {
Widget _buildEventListForDay() {
final dayEvents = _getEventsForDay(_selectedDay!);
if (dayEvents.isEmpty) {
return Center(
child: Text(
"${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程",
style: const TextStyle(color: Colors.grey),
),
child: Text("${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程", style: const TextStyle(color: Colors.grey, fontSize: 16)),
);
}
return ListView.builder(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: dayEvents.length,
itemBuilder: (context, index) {
final event = dayEvents[index];
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Container(
width: 4,
height: 40,
decoration: BoxDecoration(
color: event.levelColor,
borderRadius: BorderRadius.circular(2),
itemBuilder: (context, index) => _buildEventCard(dayEvents[index]),
);
}
// ============== 視圖 2:列表模式 (簡單式日期篩選器) ==============
Widget _buildListLayout() {
return Column(
children: [
// 頂部日期區間選擇器 (簡單式左右分割)
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(child: _buildDateSelectButton('開始日期', _listStartDate, true)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Text('', style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)),
),
),
title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(event.timeRangeText),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => CalendarDetail(event: event))
),
Expanded(child: _buildDateSelectButton('結束日期', _listEndDate, false)),
],
),
);
},
),
// 列表結果
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _listEvents.isEmpty
? const Center(child: Text("該區間尚無任何行程", style: TextStyle(color: Colors.grey, fontSize: 16)))
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
itemCount: _listEvents.length,
itemBuilder: (context, index) {
return _buildEventCard(_listEvents[index], showDate: true);
},
),
),
],
);
}
// 獨立的日期選擇按鈕 UI
Widget _buildDateSelectButton(String label, DateTime date, bool isStart) {
return InkWell(
onTap: () => _selectDate(isStart),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.05),
border: Border.all(color: Colors.blueAccent.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.calendar_today, size: 14, color: Colors.blueAccent),
const SizedBox(width: 6),
Text(
DateFormat('yyyy/MM/dd').format(date),
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blueAccent, fontSize: 14)
),
],
),
],
),
),
);
}
// ============== 共用元件:行程 Card ==============
Widget _buildEventCard(CalendarEvent event, {bool showDate = false}) {
return Card(
elevation: 0,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 5,
height: 40,
decoration: BoxDecoration(
color: event.levelColor,
borderRadius: BorderRadius.circular(4),
),
),
title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Row(
children: [
Icon(Icons.access_time, size: 14, color: Colors.grey.shade600),
const SizedBox(width: 4),
Text(
showDate && event.startTime != null
? "${DateFormat('MM/dd').format(event.startTime!)} ${event.timeRangeText}"
: event.timeRangeText,
style: TextStyle(color: Colors.grey.shade700),
),
],
),
),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => CalendarDetail(event: event))
),
),
);
}
}