modify icon & add notify function
@@ -8,7 +8,7 @@
|
|||||||
<application
|
<application
|
||||||
android:label="learn"
|
android:label="learn"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/launcher_icon"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 784 KiB |
@@ -539,7 +539,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -596,7 +596,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 791 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 27 KiB |
@@ -23,6 +23,7 @@ import './calendar/calendar_manager.dart';
|
|||||||
import './expense/expense_manager.dart';
|
import './expense/expense_manager.dart';
|
||||||
import './chart/channel_sales_manager.dart';
|
import './chart/channel_sales_manager.dart';
|
||||||
import './bpm/sign_todo_manager.dart';
|
import './bpm/sign_todo_manager.dart';
|
||||||
|
import './message/message_manager.dart';
|
||||||
|
|
||||||
class MyHttpOverrides extends HttpOverrides {
|
class MyHttpOverrides extends HttpOverrides {
|
||||||
@override
|
@override
|
||||||
@@ -162,9 +163,9 @@ class MainMenu extends StatelessWidget {
|
|||||||
targetScreen: IssueManager(),
|
targetScreen: IssueManager(),
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
title: '設定',
|
title: '訊息通知',
|
||||||
icon: Icons.settings,
|
icon: Icons.notifications,
|
||||||
targetScreen: SettingsPage(),
|
targetScreen: MessageManager(currentUserId: uid),
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
title: '費用申請',
|
title: '費用申請',
|
||||||
@@ -178,6 +179,11 @@ class MainMenu extends StatelessWidget {
|
|||||||
targetScreen: NewsManager(),
|
targetScreen: NewsManager(),
|
||||||
//targetScreen: AnnouncementManager(),
|
//targetScreen: AnnouncementManager(),
|
||||||
),
|
),
|
||||||
|
MenuItem(
|
||||||
|
title: '設定',
|
||||||
|
icon: Icons.settings,
|
||||||
|
targetScreen: SettingsPage(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import './message_model.dart';
|
||||||
|
import '../services/generic_api_service.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class MessageApiService {
|
||||||
|
final GenericApiService _apiService = GenericApiService();
|
||||||
|
|
||||||
|
// 獲取個人通知訊息紀錄 (僅顯示 30 天內)
|
||||||
|
Future<List<EipMessage>> fetchMessages(String userId) async {
|
||||||
|
// 計算 30 天前的日期,作為查詢參數
|
||||||
|
final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30));
|
||||||
|
final dateParam = DateFormat('yyyy-MM-dd').format(thirtyDaysAgo);
|
||||||
|
|
||||||
|
// 格式:當前頁^每頁筆數^排序欄位^關鍵字欄位^關鍵字內容
|
||||||
|
// 排序:按通知日期 (create_date) 降冪排列
|
||||||
|
// 實務上可根據您的 Generic API 支援度,將 dateParam 傳入作為過濾條件
|
||||||
|
// String queryFilter = "1^100^create_date^*^^^receivers^$userId^after^$dateParam";
|
||||||
|
String queryFilter = "1^100^create_date^*^^^receivers^$userId^^";
|
||||||
|
|
||||||
|
return await _apiService.fetchList<EipMessage>(
|
||||||
|
tableName: "eip_message", // 指向訊息資料表
|
||||||
|
pk: "id", //
|
||||||
|
queryFilter: queryFilter,
|
||||||
|
fromJson: (json) => EipMessage.fromJson(json),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (選用) 更新訊息為已讀狀態
|
||||||
|
Future<bool> markAsRead(int messageId) async {
|
||||||
|
// 實作呼叫更新 msg_status 的 API 邏輯
|
||||||
|
// ...
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import './message_model.dart';
|
||||||
|
|
||||||
|
class MessageDetail extends StatelessWidget {
|
||||||
|
final EipMessage message;
|
||||||
|
|
||||||
|
const MessageDetail({required this.message, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('訊息內容'),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
elevation: 0.5,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 主旨區塊
|
||||||
|
Text(
|
||||||
|
message.subjectLine,
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, height: 1.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 寄件資訊區塊
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: Colors.blueGrey.shade100,
|
||||||
|
child: const Icon(Icons.person, color: Colors.blueGrey),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message.sender, //
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
message.formattedDate,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 20.0),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 訊息內文區塊
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
message.msgContent.isNotEmpty ? message.msgContent : '無內容', //
|
||||||
|
style: const TextStyle(fontSize: 16, height: 1.6, color: Colors.black87),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import './message_model.dart';
|
||||||
|
import './message_api.dart';
|
||||||
|
import './message_detail.dart';
|
||||||
|
|
||||||
|
class MessageManager extends StatefulWidget {
|
||||||
|
final String currentUserId;
|
||||||
|
const MessageManager({required this.currentUserId, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MessageManager> createState() => _MessageManagerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageManagerState extends State<MessageManager> {
|
||||||
|
late MessageApiService _apiService;
|
||||||
|
late Future<List<EipMessage>> _messageFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_apiService = MessageApiService();
|
||||||
|
_refreshList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _refreshList() {
|
||||||
|
setState(() {
|
||||||
|
_messageFuture = _apiService.fetchMessages(widget.currentUserId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('通知訊息'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: _refreshList,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: FutureBuilder<List<EipMessage>>(
|
||||||
|
future: _messageFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
|
return const Center(child: Text('近 30 天內無任何通知訊息'));
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: snapshot.data!.length,
|
||||||
|
itemBuilder: (ctx, i) => _buildMessageCard(snapshot.data![i]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// 此為唯讀功能,因此移除 FloatingActionButton
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessageCard(EipMessage item) {
|
||||||
|
return Card(
|
||||||
|
elevation: 0.5,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () async {
|
||||||
|
// 點擊進入詳情
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => MessageDetail(message: item)),
|
||||||
|
);
|
||||||
|
// 若在詳情頁有觸發「已讀」,返回時可重新整理列表
|
||||||
|
_refreshList();
|
||||||
|
},
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: item.statusColor.withOpacity(0.1),
|
||||||
|
child: Icon(
|
||||||
|
item.isRead ? Icons.mark_email_read : Icons.mark_email_unread,
|
||||||
|
color: item.statusColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
item.subjectLine, //
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: item.isRead ? FontWeight.normal : FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(item.formattedDate),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class EipMessage {
|
||||||
|
final int id; // id (Primary Key)
|
||||||
|
final String sender; // sender (發件人)
|
||||||
|
final String receivers; // receivers (收件人)
|
||||||
|
final String subjectLine; // subject_line (主旨)
|
||||||
|
final String msgContent; // msg_content (內容)
|
||||||
|
final String msgStatus; // msg_status (閱讀狀態)
|
||||||
|
final DateTime? createDate; // create_date (通知日期)
|
||||||
|
|
||||||
|
EipMessage({
|
||||||
|
required this.id,
|
||||||
|
this.sender = '',
|
||||||
|
this.receivers = '',
|
||||||
|
this.subjectLine = '',
|
||||||
|
this.msgContent = '',
|
||||||
|
this.msgStatus = '0',
|
||||||
|
this.createDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory EipMessage.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EipMessage(
|
||||||
|
id: int.tryParse(json['id']?.toString() ?? '0') ?? 0,
|
||||||
|
sender: json['sender'] as String? ?? '系統通知',
|
||||||
|
receivers: json['receivers'] as String? ?? '',
|
||||||
|
subjectLine: json['subject_line'] as String? ?? '無主旨',
|
||||||
|
msgContent: json['msg_content'] as String? ?? '',
|
||||||
|
msgStatus: json['msg_status'] as String? ?? '0',
|
||||||
|
createDate: json['create_date'] != null ? DateTime.tryParse(json['create_date']) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化顯示通知日期
|
||||||
|
String get formattedDate {
|
||||||
|
if (createDate == null) return '未知時間';
|
||||||
|
final df = DateFormat('yyyy/MM/dd HH:mm');
|
||||||
|
return df.format(createDate!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判斷是否已讀 (假設 '1' 為已讀,'0' 為未讀)
|
||||||
|
bool get isRead => msgStatus == '1';
|
||||||
|
|
||||||
|
// 狀態視覺呈現
|
||||||
|
Color get statusColor => isRead ? Colors.grey : Colors.blueAccent;
|
||||||
|
String get statusText => isRead ? '已讀' : '未讀';
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TodoForm extends StatefulWidget {
|
||||||
|
const TodoForm({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TodoForm> createState() => _TodoFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TodoFormState extends State<TodoForm> {
|
||||||
|
final Color primaryPurple = const Color(0xFF6542D0);
|
||||||
|
final Color bgLight = const Color(0xFFF8F9FA);
|
||||||
|
|
||||||
|
// 表單資料狀態 (可視需求綁定至 API)
|
||||||
|
String _selectedGroup = 'Work';
|
||||||
|
DateTime _startDate = DateTime.now();
|
||||||
|
DateTime _endDate = DateTime.now().add(const Duration(days: 30));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: bgLight,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: bgLight,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: const Text('Add Project', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
IconButton(icon: const Icon(Icons.notifications_outlined, color: Colors.black87), onPressed: () {})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDropdownField('Task Group', _selectedGroup, Icons.work_outline),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildTextField('Project Name', 'Grocery Shopping App', isMultiline: false),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildTextField('Description', 'This application is designed for super shops...', isMultiline: true),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildDatePicker('Start Date', _startDate, true),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildDatePicker('End Date', _endDate, false),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildLogoSelector(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// 底部大型送出按鈕
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: primaryPurple,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
elevation: 5,
|
||||||
|
shadowColor: primaryPurple.withOpacity(0.5)
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// 觸發 API 儲存邏輯
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Add Project', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 輔助表單元件 ---
|
||||||
|
|
||||||
|
Widget _buildDropdownField(String label, String value, IconData icon) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
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: DropdownButton<String>(
|
||||||
|
value: value,
|
||||||
|
isExpanded: true,
|
||||||
|
icon: const Icon(Icons.arrow_drop_down, color: Colors.black54),
|
||||||
|
items: ['Work', 'Personal', 'Study'].map((String val) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: val,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(color: Colors.pink.shade50, borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(icon, color: Colors.pinkAccent, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
Text(val, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue != null) setState(() => _selectedGroup = newValue);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextField(String label, String placeholder, {bool isMultiline = false}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
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: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: placeholder,
|
||||||
|
maxLines: isMultiline ? 4 : 1,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black87),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: EdgeInsets.only(top: 8),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDatePicker(String label, DateTime date, bool isStart) {
|
||||||
|
// 實務上這裡會加上 onTap 呼叫 showDatePicker
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
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: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(color: const Color(0xFFF0EFFF), borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Icon(Icons.calendar_month, color: primaryPurple, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('01 May, 2022', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87)), // Mock 字串
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Icon(Icons.arrow_drop_down, color: Colors.black54)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLogoSelector() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
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: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(backgroundColor: Colors.teal, radius: 24, child: Text('GS', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: const [
|
||||||
|
Text('Grocery', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.teal)),
|
||||||
|
Text('shop', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFF0EFFF),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Text('Change Logo', style: TextStyle(color: primaryPurple, fontWeight: FontWeight.bold)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import './todo_api.dart';
|
import './todo_api.dart';
|
||||||
import './todo_model.dart';
|
import './todo_model.dart';
|
||||||
import './todo_detail.dart'; // 稍後創建
|
import './todo_detail.dart';
|
||||||
// import './main.dart'; // 確保可以訪問 MainMenu
|
import './todo_form.dart';
|
||||||
|
|
||||||
class TodoManager extends StatefulWidget {
|
class TodoManager extends StatefulWidget {
|
||||||
// 實際應用中,這裡應該傳入當前用戶 ID
|
|
||||||
final String currentUserId;
|
final String currentUserId;
|
||||||
const TodoManager({this.currentUserId = 'admin', super.key});
|
const TodoManager({this.currentUserId = 'admin', super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() {
|
State<StatefulWidget> createState() => _TodoManagerState();
|
||||||
return _TodoManagerState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TodoManagerState extends State<TodoManager> {
|
class _TodoManagerState extends State<TodoManager> {
|
||||||
late TodoApiService _apiService;
|
late TodoApiService _apiService;
|
||||||
late Future<List<Todo>> _todosFuture;
|
late Future<List<Todo>> _todosFuture;
|
||||||
|
|
||||||
|
// UI 狀態控制
|
||||||
|
int _selectedDateIndex = 3; // 預設選中 index 3 (即「今天」)
|
||||||
|
String _selectedFilter = 'All'; // All, To do, In Progress, Done
|
||||||
|
late List<DateTime> _dynamicDates; // 儲存動態產生的日期
|
||||||
|
|
||||||
|
final Color primaryPurple = const Color(0xFF6542D0);
|
||||||
|
final Color bgLight = const Color(0xFFF8F9FA);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_apiService = TodoApiService(currentUserId: widget.currentUserId);
|
_apiService = TodoApiService(currentUserId: widget.currentUserId);
|
||||||
// 頁面加載時自動開始獲取資料
|
|
||||||
_todosFuture = _apiService.fetchTodos();
|
_todosFuture = _apiService.fetchTodos();
|
||||||
|
_generateDates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 產生前3天到後1天的日期區間
|
||||||
|
void _generateDates() {
|
||||||
|
DateTime today = DateTime.now();
|
||||||
|
_dynamicDates = List.generate(5, (index) {
|
||||||
|
return today.subtract(Duration(days: 3 - index));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新資料的函數
|
|
||||||
void _refreshTodos() {
|
void _refreshTodos() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_todosFuture = _apiService.fetchTodos();
|
_todosFuture = _apiService.fetchTodos();
|
||||||
@@ -37,113 +50,256 @@ class _TodoManagerState extends State<TodoManager> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
backgroundColor: bgLight,
|
||||||
title: const Text('我的待辦事項'),
|
body: SafeArea(
|
||||||
actions: <Widget>[
|
child: Column(
|
||||||
IconButton(
|
children: [
|
||||||
icon: const Icon(Icons.refresh),
|
_buildCustomHeader(),
|
||||||
tooltip: '刷新列表',
|
_buildDateSelector(),
|
||||||
onPressed: _refreshTodos,
|
_buildFilterChips(),
|
||||||
),
|
Expanded(
|
||||||
// 假設這裡使用 Navigator.pop(context) 即可返回 MainMenu
|
child: _buildTodoListBody(),
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.home),
|
|
||||||
tooltip: '返回主頁',
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: FutureBuilder<List<Todo>>(
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const TodoForm()));
|
||||||
|
},
|
||||||
|
backgroundColor: primaryPurple,
|
||||||
|
elevation: 4,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
child: const Icon(Icons.add, color: Colors.white, size: 28),
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
||||||
|
bottomNavigationBar: _buildBottomNav(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI 元件區塊 ---
|
||||||
|
|
||||||
|
Widget _buildCustomHeader() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
"Today's Tasks",
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
),
|
||||||
|
// 移除小鈴鐺圖示後,保留一個佔位符號以確保標題能完美置中
|
||||||
|
const SizedBox(width: 48),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateSelector() {
|
||||||
|
return SizedBox(
|
||||||
|
height: 90,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
itemCount: _dynamicDates.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final date = _dynamicDates[index];
|
||||||
|
final isSelected = index == _selectedDateIndex;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _selectedDateIndex = index),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
width: 65,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? primaryPurple : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
if (!isSelected) BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat('MMM').format(date), style: TextStyle(color: isSelected ? Colors.white70 : Colors.grey, fontSize: 12)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(DateFormat('dd').format(date), style: TextStyle(color: isSelected ? Colors.white : Colors.black, fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(DateFormat('EEE').format(date), style: TextStyle(color: isSelected ? Colors.white70 : Colors.grey, fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterChips() {
|
||||||
|
final filters = ['All', 'To do', 'In Progress', 'Done'];
|
||||||
|
return SizedBox(
|
||||||
|
height: 60,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
itemCount: filters.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isSelected = _selectedFilter == filters[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 12),
|
||||||
|
child: ChoiceChip(
|
||||||
|
label: Text(filters[index]),
|
||||||
|
selected: isSelected,
|
||||||
|
onSelected: (bool selected) {
|
||||||
|
if (selected) setState(() => _selectedFilter = filters[index]);
|
||||||
|
},
|
||||||
|
selectedColor: primaryPurple,
|
||||||
|
backgroundColor: const Color(0xFFF0EFFF),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: isSelected ? Colors.white : primaryPurple,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide.none),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTodoListBody() {
|
||||||
|
return FutureBuilder<List<Todo>>(
|
||||||
future: _todosFuture,
|
future: _todosFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('載入失敗: ${snapshot.error}'));
|
||||||
|
} else if (snapshot.hasData) {
|
||||||
|
|
||||||
|
// 取得目前選取的時間
|
||||||
|
DateTime selectedDate = _dynamicDates[_selectedDateIndex];
|
||||||
|
|
||||||
|
List<Todo> filteredTodos = snapshot.data!.where((todo) {
|
||||||
|
// 1. 狀態過濾邏輯
|
||||||
|
bool statusMatch = true;
|
||||||
|
if (_selectedFilter != 'All') {
|
||||||
|
String status = (todo.status ?? '').toUpperCase();
|
||||||
|
if (_selectedFilter == 'Done') statusMatch = status == 'DONE';
|
||||||
|
else if (_selectedFilter == 'In Progress') statusMatch = status == 'WIP';
|
||||||
|
else if (_selectedFilter == 'To do') statusMatch = status != 'DONE' && status != 'WIP';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 日期過濾邏輯 (比對 年、月、日)
|
||||||
|
bool dateMatch = false;
|
||||||
|
if (todo.endDate != null) {
|
||||||
|
dateMatch = (todo.endDate!.year == selectedDate.year) &&
|
||||||
|
(todo.endDate!.month == selectedDate.month) &&
|
||||||
|
(todo.endDate!.day == selectedDate.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusMatch && dateMatch;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (filteredTodos.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Text('此日期沒有「$_selectedFilter」狀態的任務。',
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
style: const TextStyle(color: Colors.grey, fontSize: 16)),
|
||||||
children: [
|
);
|
||||||
Text('載入失敗: ${snapshot.error}', textAlign: TextAlign.center),
|
}
|
||||||
const SizedBox(height: 16),
|
|
||||||
ElevatedButton(onPressed: _refreshTodos, child: const Text('重試')),
|
return RefreshIndicator(
|
||||||
],
|
onRefresh: () async => _refreshTodos(),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 10, 20, 80),
|
||||||
|
itemCount: filteredTodos.length,
|
||||||
|
itemBuilder: (context, index) => _buildTaskCard(filteredTodos[index]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
|
||||||
return TodoList(todos: snapshot.data!);
|
|
||||||
} else {
|
} else {
|
||||||
return const Center(child: Text('目前沒有待辦事項。工作很輕鬆!'));
|
return const Center(child: Text('目前沒有待辦事項。工作很輕鬆!'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------
|
Widget _buildTaskCard(Todo item) {
|
||||||
// 列表顯示小部件 (TodoList)
|
String displayStatus = 'To-do';
|
||||||
// -----------------------------------------------------------
|
Color chipColor = Colors.blueGrey;
|
||||||
|
Color chipBg = Colors.blueGrey.withOpacity(0.1);
|
||||||
|
|
||||||
class TodoList extends StatelessWidget {
|
if (item.status?.toUpperCase() == 'DONE') {
|
||||||
final List<Todo> todos;
|
displayStatus = 'Done';
|
||||||
|
chipColor = primaryPurple;
|
||||||
|
chipBg = primaryPurple.withOpacity(0.1);
|
||||||
|
} else if (item.status?.toUpperCase() == 'WIP') {
|
||||||
|
displayStatus = 'In Progress';
|
||||||
|
chipColor = Colors.orange;
|
||||||
|
chipBg = Colors.orange.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
|
||||||
const TodoList({required this.todos, super.key});
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
@override
|
padding: const EdgeInsets.all(16),
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: todos.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final item = todos[index];
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 3,
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
// 點擊項目:導航到詳細頁面
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => TodoDetail(todo: item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: ListTile(
|
|
||||||
// 左側狀態指示器
|
|
||||||
leading: Container(
|
|
||||||
width: 10,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: item.statusColor,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.08), blurRadius: 15, offset: const Offset(0, 5))],
|
||||||
),
|
),
|
||||||
),
|
child: InkWell(
|
||||||
title: Text(
|
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => TodoDetail(todo: item))),
|
||||||
item.taskName,
|
child: Column(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
'分類: ${item.className} | 優先級: ${item.priority ?? '一般'}',
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
trailing: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
'截止日',
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: TextStyle(fontSize: 10, color: item.statusColor),
|
children: [
|
||||||
|
Text(item.className, style: const TextStyle(color: Colors.grey, fontSize: 13, fontWeight: FontWeight.w500)),
|
||||||
|
Icon(Icons.work_outline, color: primaryPurple.withOpacity(0.5), size: 18),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(height: 8),
|
||||||
item.formattedEndDate,
|
Text(item.taskName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)),
|
||||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.access_time_filled, color: Colors.grey, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(item.formattedEndDate, style: const TextStyle(color: Colors.grey, fontSize: 13, fontWeight: FontWeight.w500)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(color: chipBg, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Text(displayStatus, style: TextStyle(color: chipColor, fontSize: 12, fontWeight: FontWeight.w700)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// 移除了內部的圖示,但保留了 BottomAppBar 的挖空設計與高度
|
||||||
|
Widget _buildBottomNav() {
|
||||||
|
return const BottomAppBar(
|
||||||
|
shape: CircularNotchedRectangle(),
|
||||||
|
notchMargin: 8.0,
|
||||||
|
color: Color(0xFFF0EFFF),
|
||||||
|
elevation: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 60, // 維持底部導覽列的高度,留給 FAB 空間
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
1.先上傳 3張圖,在上傳原先開發的版本程式
|
||||||
|
然後,呼叫 Gemini 3.0 下 prompt
|
||||||
|
|
||||||
|
請將原本開發好的 ux 改為新上傳的樣式,api 及欄位不變,只是參考 ux 修改
|
||||||
|
|
||||||
|
>> 就會產生新版程式
|
||||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -1,6 +1,22 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -21,10 +37,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -150,6 +182,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -240,6 +280,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -312,6 +360,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.2"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.11.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -348,18 +404,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -408,6 +464,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -424,6 +488,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -545,10 +617,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.9"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -661,6 +733,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -62,6 +62,17 @@ dev_dependencies:
|
|||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
flutter_launcher_icons: ^0.13.1 # 請檢查最新版本
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: "launcher_icon"
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/images/app_icon.png"
|
||||||
|
min_sdk_android: 21 # android support for adaptive icons
|
||||||
|
|
||||||
|
# 如果需要 Android 自適應圖示 (Adaptive Icons),可額外配置:
|
||||||
|
# adaptive_icon_background: "#FFFFFF"
|
||||||
|
# adaptive_icon_foreground: "assets/images/app_icon_foreground.png"
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||