diff --git a/lib/activity.dart b/lib/activity.dart new file mode 100644 index 0000000..258a5ac --- /dev/null +++ b/lib/activity.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; + +class ActivityListPage extends StatefulWidget { + const ActivityListPage({Key? key}) : super(key: key); + + @override + State createState() => _ActivityListPageState(); +} + +class _ActivityListPageState extends State { + int? selectedActivityId; + int selectedPeopleCount = 1; + + void openRegisterModal(int activityId) { + setState(() { + selectedActivityId = activityId; + }); + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('確認報名'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('選擇報名人數:'), + DropdownButton( + value: selectedPeopleCount, + onChanged: (value) { + if (value != null) { + setState(() { + selectedPeopleCount = value; + }); + } + }, + items: + [1, 2, 3, 4] + .map( + (e) => + DropdownMenuItem(value: e, child: Text('$e 人')), + ) + .toList(), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '已報名活動 ID $selectedActivityId,人數:$selectedPeopleCount', + ), + ), + ); + }, + child: const Text('確認報名'), + ), + ], + ), + ); + } + + Widget activityCard({ + required String title, + required String time, + required String location, + int? id, + bool canRegister = false, + }) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text(time), + Text(location), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + // TODO: 導向詳情頁 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + foregroundColor: Colors.black, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Colors.black), + ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + ), + child: const Text('查看詳情'), + ), + const SizedBox(width: 8), + if (canRegister && id != null) + ElevatedButton( + onPressed: () => openRegisterModal(id), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + ), + child: const Text('我要報名'), + ), + ], + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), // 上方圓角 + ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + children: [ + // 顶部滑块 + const SizedBox(height: 12), + Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), + ), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + const Text( + '社區活動列表', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + Expanded( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 12.0, + right: 16, + left: 16, + ), + child: Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () { + // TODO: 導向提交活動頁 + }, + icon: const Icon(Icons.add), + label: const Text('提交活動申請'), + ), + ), + ), + activityCard( + title: '🎉 社區春季市集', + time: '時間:2025/04/27(日)10:00 - 16:00', + location: '地點:中庭花園', + id: 1, + ), + activityCard( + title: '🎉 早晨瑜珈課程', + time: '每週六 07:00 - 08:00', + location: '地點:社區多功能教室', + id: 2, + canRegister: true, + ), + activityCard( + title: '🎉 二手書交換日', + time: '2025/05/05(六)13:00 - 17:00', + location: '地點:社區圖書區', + id: 3, + canRegister: true, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/bill.dart b/lib/bill.dart new file mode 100644 index 0000000..46e2ddc --- /dev/null +++ b/lib/bill.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; + +class BillItem { + final String date; + final String title; + final int amount; + final String dueDate; + final String? reminder; + + BillItem({ + required this.date, + required this.title, + required this.amount, + required this.dueDate, + this.reminder, + }); +} + +class BillPage extends StatelessWidget { + final ScrollController scrollController; + + BillPage({super.key, required this.scrollController}); + + final List bills = [ + BillItem( + date: '2025/04/22', + title: '管理費', + amount: 1200, + dueDate: '2025/04/30', + reminder: '第 2 次催繳', + ), + BillItem( + date: '2025/04/20', + title: '水費', + amount: 340, + dueDate: '2025/04/28', + ), + BillItem( + date: '2025/04/18', + title: '停車費', + amount: 2000, + dueDate: '2025/04/25', + reminder: '第 3 次催繳', + ), + ]; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFF9EAF9F), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 8), + const Text( + '繳費通知', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.white, + ), + ), + ], + ), + ), + Expanded( + child: Container( + color: const Color(0xFFF7F8FA), + padding: const EdgeInsets.all(16), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + controller: scrollController, + children: [ + const Text( + '💰 繳費項目', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + ...bills.map((bill) => BillTile(bill: bill)).toList(), + ], + ), + ), + ), + ), + ), + ], + ); + } +} + +class BillTile extends StatelessWidget { + final BillItem bill; + + const BillTile({super.key, required this.bill}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Color(0xFFDDDDDD))), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + bill.date, + style: const TextStyle(fontSize: 14, color: Color(0xFF555555)), + ), + Text( + '${bill.title} - \$${bill.amount}', + style: const TextStyle(fontSize: 14, color: Color(0xFF222222)), + ), + ], + ), + const SizedBox(height: 4), + Text( + '繳費截止日:${bill.dueDate}', + style: const TextStyle(fontSize: 13, color: Color(0xFF777777)), + ), + if (bill.reminder != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + bill.reminder!, + style: TextStyle( + fontSize: 13, + color: Colors.red[700], + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} + +// 👇 浮層包裝元件 +class BillPageWrapper extends StatelessWidget { + const BillPageWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.95, + minChildSize: 0.5, + maxChildSize: 1.0, + expand: false, + builder: (_, scrollController) { + return Container( + color: Colors.transparent, + child: BillPage(scrollController: scrollController), + ); + }, + ); + } +} diff --git a/lib/emergency.dart b/lib/emergency.dart new file mode 100644 index 0000000..1b9dee8 --- /dev/null +++ b/lib/emergency.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; + +class EmergencyPage extends StatefulWidget { + const EmergencyPage({super.key}); + + @override + State createState() => _EmergencyPageState(); +} + +class _EmergencyPageState extends State { + final TextEditingController _descriptionController = TextEditingController(); + String? _selectedType; + + final List> disasterTypes = [ + {"label": "🔥 火災", "value": "火災"}, + {"label": "🌏 地震", "value": "地震"}, + {"label": "💧 水災", "value": "水災"}, + {"label": "🕵️‍♂️ 可疑人物", "value": "可疑人物"}, + {"label": "⚠️ 公共設施故障", "value": "公共設施故障"}, + {"label": "❓ 其他", "value": "其他"}, + ]; + + void _sendAlert() { + final type = _selectedType; + final desc = _descriptionController.text.trim(); + + if (type == null || type.isEmpty) { + _showDialog("⚠️ 請先選擇災害類型!"); + return; + } + + if (desc.isEmpty) { + _showDialog("⚠️ 請輸入簡易說明!"); + return; + } + + _showDialog("✅ 已通報「$type」\n說明:「$desc」"); + } + + void _showDialog(String message) { + showDialog( + context: context, + builder: + (_) => AlertDialog( + content: Text(message), + actions: [ + TextButton( + child: const Text('確定'), + onPressed: () { + Navigator.pop(context); // 關閉 dialog + Navigator.pop(context); // 關閉 bottomSheet + }, + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 5, + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), + ), + ), + Image.network( + 'https://cdn-icons-png.flaticon.com/512/564/564619.png', + width: 100, + ), + const SizedBox(height: 15), + const Text('若遇緊急情況,請選擇災害類型後立即通報。'), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Text( + '災害類型', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + decoration: const InputDecoration(border: OutlineInputBorder()), + value: _selectedType, + hint: const Text("請選擇..."), + items: + disasterTypes + .map( + (item) => DropdownMenuItem( + value: item['value'], + child: Text(item['label']!), + ), + ) + .toList(), + onChanged: (value) { + setState(() { + _selectedType = value; + }); + }, + ), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Text( + '簡易說明', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 8), + TextField( + controller: _descriptionController, + maxLines: 3, + decoration: const InputDecoration( + hintText: '請輸入簡要說明,例如地點或狀況...', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 25), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _sendAlert, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + icon: const Icon(Icons.warning), + label: const Text('立即通報'), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/package.dart b/lib/package.dart new file mode 100644 index 0000000..826fefb --- /dev/null +++ b/lib/package.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; + +class PackagePage extends StatelessWidget { + final ScrollController scrollController; + + const PackagePage({super.key, required this.scrollController}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // 自訂頂部 Bar + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFF9EAF9F), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 8), + const Text( + '信件包裹通知', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.white, + ), + ), + ], + ), + ), + + // 可滾動內容 + Expanded( + child: Container( + color: const Color(0xFFF7F8FA), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: ListView( + controller: scrollController, + children: [ + const Text( + '📦 待領包裹', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + _buildParcelItem( + context: context, + date: '2025/04/16', + courier: '宅配 - 7-11 交貨便', + recipient: '林小安', + parcelId: 'P20240416001', + amount: 250, + ), + _buildParcelItem( + context: context, + date: '2025/04/14', + courier: '黑貓宅急便', + recipient: '林小安', + parcelId: 'P20240414002', + amount: 120, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildParcelItem({ + required String date, + required String courier, + required String recipient, + required String parcelId, + required int amount, + required BuildContext context, + }) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + margin: const EdgeInsets.only(bottom: 16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Color(0x11000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRow(date, courier), + const SizedBox(height: 4), + _buildRow('收件人:$recipient', '包裹編號:$parcelId'), + const SizedBox(height: 4), + _buildRow('代收金額:\$${amount.toString()}', ''), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF4CAF50), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + onPressed: () { + showDialog( + context: context, + builder: + (ctx) => AlertDialog( + title: const Text('包裹已領取'), + content: const Text('您已確認領取此包裹。'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('確定'), + ), + ], + ), + ); + }, + child: const Text('確認領取', style: TextStyle(fontSize: 13)), + ), + ), + ], + ), + ), + ); + } + + Widget _buildRow(String left, String right) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + left, + style: const TextStyle(fontSize: 14, color: Color(0xFF555555)), + ), + Text( + right, + style: const TextStyle(fontSize: 14, color: Color(0xFF555555)), + ), + ], + ); + } +} + +class PackagePageWrapper extends StatelessWidget { + const PackagePageWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.95, + minChildSize: 0.5, + maxChildSize: 1.0, + expand: false, + builder: (_, scrollController) { + return Container( + decoration: const BoxDecoration(color: Colors.transparent), + child: PackagePage(scrollController: scrollController), + ); + }, + ); + } +} diff --git a/lib/visitor.dart b/lib/visitor.dart new file mode 100644 index 0000000..e689bf0 --- /dev/null +++ b/lib/visitor.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; + +class VisitorPage extends StatelessWidget { + final ScrollController scrollController; + + const VisitorPage({super.key, required this.scrollController}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFF9EAF9F), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 8), + const Text( + '訪客來訪紀錄', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.white, + ), + ), + ], + ), + ), + Expanded( + child: Container( + color: const Color(0xFFF7F8FA), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: ListView( + controller: scrollController, + children: [ + const Text( + '📝 訪客紀錄', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + _buildVisitorItem('2025/04/22 14:35', '王小明', '身分:朋友|目的:拜訪林小安'), + _buildVisitorItem('2025/04/21 10:20', '陳美麗', '身分:水電師傅|目的:維修浴室'), + _buildVisitorItem( + '2025/04/20 17:45', + '李建國', + '身分:外送員|目的:送晚餐(Uber Eats)', + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildVisitorItem(String date, String name, String detail) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Color(0xFFEEEEEE))), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + date, + style: const TextStyle(fontSize: 14, color: Color(0xFF555555)), + ), + Text( + name, + style: const TextStyle(fontSize: 14, color: Color(0xFF555555)), + ), + ], + ), + const SizedBox(height: 4), + Text( + detail, + style: const TextStyle(fontSize: 13, color: Color(0xFF777777)), + ), + ], + ), + ); + } +} + +// 👇 包裝元件一樣寫在這 +class VisitorPageWrapper extends StatelessWidget { + const VisitorPageWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.95, + minChildSize: 0.5, + maxChildSize: 1.0, + expand: false, + builder: (_, scrollController) { + return Container( + decoration: const BoxDecoration(color: Colors.transparent), + child: VisitorPage(scrollController: scrollController), + ); + }, + ); + } +}