2025-05-09 11:46:02 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
2025-05-09 13:23:42 +08:00
|
|
|
|
import 'login_page.dart';
|
2025-05-09 11:46:02 +08:00
|
|
|
|
|
|
|
|
|
class NewResidentStepPage extends StatefulWidget {
|
|
|
|
|
const NewResidentStepPage({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<NewResidentStepPage> createState() => _NewResidentStepPageState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _NewResidentStepPageState extends State<NewResidentStepPage> {
|
|
|
|
|
int currentStep = 1;
|
|
|
|
|
final TextEditingController _emailController = TextEditingController();
|
|
|
|
|
final List<TextEditingController> _codeControllers = List.generate(
|
|
|
|
|
6,
|
|
|
|
|
(_) => TextEditingController(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
String? statusMessage;
|
|
|
|
|
bool isSuccess = false;
|
|
|
|
|
|
|
|
|
|
void sendVerification() {
|
|
|
|
|
final email = _emailController.text.trim();
|
|
|
|
|
if (email.contains("@")) {
|
|
|
|
|
setState(() {
|
|
|
|
|
isSuccess = true;
|
|
|
|
|
statusMessage = '📧 驗證碼已寄出至 $email';
|
|
|
|
|
currentStep = 2;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setState(() {
|
|
|
|
|
isSuccess = false;
|
|
|
|
|
statusMessage = '❌ 請輸入有效的 Email';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void verifyCode() {
|
|
|
|
|
final code = _codeControllers.map((c) => c.text).join();
|
|
|
|
|
if (RegExp(r'^\d{6}$').hasMatch(code)) {
|
|
|
|
|
setState(() {
|
|
|
|
|
isSuccess = true;
|
|
|
|
|
statusMessage = '✅ 驗證成功!';
|
|
|
|
|
currentStep = 3;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setState(() {
|
|
|
|
|
isSuccess = false;
|
|
|
|
|
statusMessage = '❌ 請輸入正確的 6 位數驗證碼';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget buildStep1() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'步驟 1 之 3',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
const Text(
|
|
|
|
|
'輸入電子郵件',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: _emailController,
|
|
|
|
|
keyboardType: TextInputType.emailAddress,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '電子郵件',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
hintText: 'example@mail.com',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
onPressed: sendVerification,
|
|
|
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.indigo),
|
|
|
|
|
child: const Text('發送驗證碼', style: TextStyle(color: Colors.white)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget buildStep2() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'步驟 2 之 3',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
const Text(
|
|
|
|
|
'請輸入驗證碼',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
const Text(
|
|
|
|
|
'我們已將 6 位數驗證碼寄至您的 Email',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(color: Colors.grey),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: List.generate(6, (i) {
|
|
|
|
|
return Container(
|
|
|
|
|
width: 40,
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
|
|
|
child: TextField(
|
|
|
|
|
controller: _codeControllers[i],
|
|
|
|
|
maxLength: 1,
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
keyboardType: TextInputType.number,
|
|
|
|
|
decoration: const InputDecoration(counterText: ''),
|
|
|
|
|
onChanged: (val) {
|
|
|
|
|
if (val.length == 1 && i < 5) {
|
|
|
|
|
FocusScope.of(context).nextFocus();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
onPressed: verifyCode,
|
|
|
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.indigo),
|
|
|
|
|
child: const Text('驗證並繼續', style: TextStyle(color: Colors.white)),
|
|
|
|
|
),
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
statusMessage = '📨 驗證碼已重新寄出,請再次查收 Email。';
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
child: const Text('重新發送驗證碼'),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 在 State 裡新增這些 controller 和變數:
|
|
|
|
|
final nameController = TextEditingController();
|
|
|
|
|
final birthdayController = TextEditingController();
|
|
|
|
|
final phoneController = TextEditingController();
|
|
|
|
|
final roomController = TextEditingController();
|
|
|
|
|
final plateController = TextEditingController();
|
|
|
|
|
final passwordController = TextEditingController();
|
|
|
|
|
final confirmPasswordController = TextEditingController();
|
|
|
|
|
String? selectedGender;
|
|
|
|
|
|
|
|
|
|
// 表單驗證 function
|
|
|
|
|
bool validateStep3Fields() {
|
|
|
|
|
return true;
|
|
|
|
|
/*
|
|
|
|
|
nameController.text.isNotEmpty &&
|
|
|
|
|
selectedGender != null &&
|
|
|
|
|
birthdayController.text.isNotEmpty &&
|
|
|
|
|
phoneController.text.isNotEmpty &&
|
|
|
|
|
roomController.text.isNotEmpty &&
|
|
|
|
|
plateController.text.isNotEmpty &&
|
|
|
|
|
passwordController.text.isNotEmpty &&
|
|
|
|
|
confirmPasswordController.text.isNotEmpty &&
|
|
|
|
|
passwordController.text == confirmPasswordController.text;
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildStep3 widget
|
|
|
|
|
Widget buildStep3() {
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'步驟 3 之 3',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
const Text(
|
|
|
|
|
'填寫基本資料',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
const Text(
|
|
|
|
|
'請輸入您的基本資訊以完成註冊',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(color: Colors.grey),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
|
|
|
|
TextField(
|
|
|
|
|
controller: nameController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '姓名',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
DropdownButtonFormField<String>(
|
|
|
|
|
value: selectedGender,
|
|
|
|
|
items: const [
|
|
|
|
|
DropdownMenuItem(value: '男', child: Text('男')),
|
|
|
|
|
DropdownMenuItem(value: '女', child: Text('女')),
|
|
|
|
|
DropdownMenuItem(value: '其他', child: Text('其他')),
|
|
|
|
|
],
|
|
|
|
|
onChanged: (value) {
|
|
|
|
|
setState(() {
|
|
|
|
|
selectedGender = value;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '性別',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: birthdayController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '生日',
|
|
|
|
|
hintText: 'YYYY-MM-DD',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
keyboardType: TextInputType.datetime,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: phoneController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '手機號碼',
|
|
|
|
|
hintText: '例如:0912345678',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
keyboardType: TextInputType.phone,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: roomController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '房號 / 室別',
|
|
|
|
|
hintText: '例如:A棟 5F-2',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: plateController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '車牌號碼',
|
|
|
|
|
hintText: '例如:ABC-1234',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: passwordController,
|
|
|
|
|
obscureText: true,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '密碼',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextField(
|
|
|
|
|
controller: confirmPasswordController,
|
|
|
|
|
obscureText: true,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: '確認密碼',
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
|
ElevatedButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
if (validateStep3Fields()) {
|
|
|
|
|
setState(() {
|
|
|
|
|
currentStep = 4;
|
|
|
|
|
statusMessage = null;
|
|
|
|
|
isSuccess = true;
|
|
|
|
|
});
|
|
|
|
|
ScaffoldMessenger.of(
|
|
|
|
|
context,
|
|
|
|
|
).showSnackBar(const SnackBar(content: Text('✅ 註冊完成!歡迎加入社區!')));
|
|
|
|
|
} else {
|
|
|
|
|
setState(() {
|
|
|
|
|
isSuccess = false;
|
|
|
|
|
statusMessage = '❌ 請完整填寫所有欄位,並確認密碼一致';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
|
|
|
|
|
child: const Text('完成註冊', style: TextStyle(color: Colors.white)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget buildStep4() {
|
|
|
|
|
return Column(
|
|
|
|
|
children: [
|
|
|
|
|
// Body content
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min, // 讓 Column 的高度根據內容來調整
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'親愛的用戶您好',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 22,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: Color(0xFF333333),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
const Text(
|
|
|
|
|
'您的新用戶申請已送至管理室,請您拿手機至管理室由管理員掃描開通。',
|
|
|
|
|
style: TextStyle(fontSize: 16, color: Color(0xFF666666)),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 40),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: () {
|
2025-05-09 13:23:42 +08:00
|
|
|
|
setState(() {
|
|
|
|
|
currentStep = 5;
|
|
|
|
|
statusMessage = null;
|
|
|
|
|
isSuccess = true;
|
|
|
|
|
});
|
2025-05-09 11:46:02 +08:00
|
|
|
|
},
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: const Color(0xFF4CAF50),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(24),
|
|
|
|
|
),
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
|
|
|
),
|
|
|
|
|
child: const Text(
|
|
|
|
|
'掃描開通',
|
|
|
|
|
style: TextStyle(fontSize: 16, color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 13:23:42 +08:00
|
|
|
|
Widget buildStep5() {
|
|
|
|
|
return Column(
|
|
|
|
|
children: [
|
|
|
|
|
// Body content
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min, // 讓 Column 的高度根據內容來調整
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'掃描QR Code 來開通',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 22,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: Color(0xFF333333),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
const Text(
|
|
|
|
|
'請掃描下面的 QR Code 開通此住戶。',
|
|
|
|
|
style: TextStyle(fontSize: 16, color: Color(0xFF666666)),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
// 塞入QR CODE 圖片
|
|
|
|
|
Image.network(
|
|
|
|
|
'https://docs.lightburnsoftware.com/legacy/img/QRCode/ExampleCode.png',
|
|
|
|
|
width: 200,
|
|
|
|
|
height: 200,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 40),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Navigator.pushReplacement(
|
|
|
|
|
context,
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
builder: (context) => const LoginPage(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: const Color(0xFF4CAF50),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(24),
|
|
|
|
|
),
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
|
|
|
),
|
|
|
|
|
child: const Text(
|
|
|
|
|
'掃描完畢,返回登入介面',
|
|
|
|
|
style: TextStyle(fontSize: 16, color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const Text(
|
|
|
|
|
'如果無法掃描,請調整螢幕亮度。',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
color: Color.fromARGB(255, 129, 129, 129),
|
|
|
|
|
),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 11:46:02 +08:00
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
backgroundColor: const Color(0xFFF7F8FA),
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
backgroundColor: const Color(0xFF9EAF9F),
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
title: const Text('新住戶註冊'),
|
|
|
|
|
leading:
|
|
|
|
|
currentStep > 1
|
|
|
|
|
? IconButton(
|
|
|
|
|
icon: const Icon(Icons.arrow_back),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
currentStep--;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
: null,
|
|
|
|
|
),
|
|
|
|
|
body: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 3)],
|
|
|
|
|
),
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
buildStepContent(),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
if (statusMessage != null)
|
|
|
|
|
Text(
|
|
|
|
|
statusMessage!,
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: isSuccess ? Colors.green : Colors.red,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget buildStepContent() {
|
|
|
|
|
switch (currentStep) {
|
|
|
|
|
case 1:
|
|
|
|
|
return buildStep1();
|
|
|
|
|
case 2:
|
|
|
|
|
return buildStep2();
|
|
|
|
|
case 3:
|
|
|
|
|
return buildStep3();
|
|
|
|
|
case 4:
|
|
|
|
|
return buildStep4();
|
2025-05-09 13:23:42 +08:00
|
|
|
|
case 5:
|
|
|
|
|
return buildStep5();
|
2025-05-09 11:46:02 +08:00
|
|
|
|
default:
|
|
|
|
|
return const Text('未知步驟');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|