First Commit
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import './house_note_model.dart';
|
||||
import './house_note_api.dart';
|
||||
import './house_photo_form.dart';
|
||||
import './house_tag_form.dart';
|
||||
import './house_evaluation_form.dart';
|
||||
|
||||
class HouseNoteDetail extends StatefulWidget {
|
||||
final House house;
|
||||
const HouseNoteDetail({required this.house, super.key});
|
||||
|
||||
@override
|
||||
State<HouseNoteDetail> createState() => _HouseNoteDetailState();
|
||||
}
|
||||
|
||||
class _HouseNoteDetailState extends State<HouseNoteDetail> {
|
||||
final HouseNoteApiService _api = HouseNoteApiService();
|
||||
|
||||
List<HouseEvaluation> _evaluations = [];
|
||||
List<HousePhoto> _photos = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAllData();
|
||||
}
|
||||
|
||||
// 同步抓取評估紀錄與照片
|
||||
Future<void> _loadAllData() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final results = await Future.wait([
|
||||
_api.fetchEvaluations(widget.house.id),
|
||||
_api.fetchPhotos(widget.house.id),
|
||||
]);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_evaluations = results[0] as List<HouseEvaluation>;
|
||||
_photos = results[1] as List<HousePhoto>;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('資料載入失敗: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('賞屋詳細內容'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadAllData,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// a. 基本資料區塊
|
||||
_buildSectionHeader('房屋基本資料', Icons.info_outline),
|
||||
_buildBasicInfoCard(),
|
||||
|
||||
// b. 屋況檢查區塊 (與照片主鍵關聯)
|
||||
_buildSectionHeader('屋況檢查評估', Icons.checklist_rtl),
|
||||
_buildEvaluationList(),
|
||||
|
||||
// c. 照片總覽區塊
|
||||
_buildSectionHeader('照片總覽 (點擊可放大)', Icons.photo_library_outlined),
|
||||
_buildPhotoGrid(),
|
||||
|
||||
const SizedBox(height: 100), // 留白空間
|
||||
],
|
||||
),
|
||||
),
|
||||
// 底部懸浮按鈕區
|
||||
bottomSheet: _buildBottomActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
// --- UI 元件函式 ---
|
||||
|
||||
// 分區標題
|
||||
Widget _buildSectionHeader(String title, IconData icon) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
color: Colors.grey.shade100,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: Colors.blueAccent),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 基本資料卡片
|
||||
Widget _buildBasicInfoCard() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(Icons.home, '物件名稱', widget.house.title),
|
||||
const Divider(),
|
||||
_buildDetailRow(Icons.location_on, '完整地址', widget.house.address ?? '未填寫'),
|
||||
const Divider(),
|
||||
_buildDetailRow(Icons.monetization_on, '預估價格', widget.house.priceTag),
|
||||
const Divider(),
|
||||
_buildDetailRow(Icons.layers, '物件規格', widget.house.infoSummary),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 輔助明細列
|
||||
Widget _buildDetailRow(IconData icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 屋況檢查清單
|
||||
Widget _buildEvaluationList() {
|
||||
if (_evaluations.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(30),
|
||||
child: Text('目前尚無評估數據', style: TextStyle(color: Colors.grey)),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _evaluations.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final eval = _evaluations[index];
|
||||
// 關鍵:找出有沒有照片是關聯到這個評估項目的 (透過 evaluation_id)
|
||||
final HousePhoto? linkedPhoto = _photos.cast<HousePhoto?>().firstWhere(
|
||||
(p) => p?.evaluationId == eval.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text(eval.itemName, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(5, (i) => Icon(
|
||||
Icons.star,
|
||||
size: 16,
|
||||
color: i < eval.score ? Colors.orange : Colors.grey.shade300
|
||||
)),
|
||||
),
|
||||
if (eval.itemMemo != null && eval.itemMemo!.isNotEmpty)
|
||||
Text(eval.itemMemo!, style: const TextStyle(fontSize: 12, color: Colors.blueGrey)),
|
||||
],
|
||||
),
|
||||
trailing: linkedPhoto != null
|
||||
? GestureDetector(
|
||||
onTap: () => _showFullScreenImage(context, linkedPhoto.fullUrl),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image.network(
|
||||
linkedPhoto.fullUrl,
|
||||
width: 50, height: 50,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 30),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 照片總覽 Grid
|
||||
Widget _buildPhotoGrid() {
|
||||
if (_photos.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(30),
|
||||
child: Text('尚未拍攝照片', style: TextStyle(color: Colors.grey)),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: _photos.length,
|
||||
itemBuilder: (context, index) {
|
||||
final photo = _photos[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _showFullScreenImage(context, photo.fullUrl),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
photo.fullUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
color: Colors.grey.shade200,
|
||||
child: const Icon(Icons.broken_image, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- 互動功能 ---
|
||||
|
||||
// 圖片放大功能 (支援雙指縮放)
|
||||
void _showFullScreenImage(BuildContext context, String imageUrl) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(backgroundColor: Colors.black, foregroundColor: Colors.white),
|
||||
body: Center(
|
||||
child: InteractiveViewer(
|
||||
panEnabled: true,
|
||||
minScale: 0.5,
|
||||
maxScale: 4.0,
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return const CircularProgressIndicator(color: Colors.white);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 底部按鈕
|
||||
Widget _buildBottomActions(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade200)),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)]
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HouseEvaluationForm(
|
||||
houseId: widget.house.id,
|
||||
houseTitle: widget.house.title,
|
||||
),
|
||||
),
|
||||
);
|
||||
// 返回後刷新詳情頁
|
||||
if (result == true) {
|
||||
_loadAllData();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.rate_review),
|
||||
label: const Text('賞屋評估'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 保持與拍照按鈕一致的圓角
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HousePhotoForm(
|
||||
houseId: widget.house.id,
|
||||
houseTitle: widget.house.title
|
||||
)
|
||||
)
|
||||
);
|
||||
// 拍完照回來自動刷新頁面
|
||||
if (result == true) {
|
||||
_loadAllData();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
label: const Text('拍照紀錄'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 保持與拍照按鈕一致的圓角
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
// 跳轉至標籤編輯頁面 (HouseTagForm)
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HouseTagForm(
|
||||
houseId: widget.house.id,
|
||||
houseTitle: widget.house.title,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 當從標籤頁面返回,且 result 為 true 時,自動刷新詳情頁資料
|
||||
if (result == true) {
|
||||
_loadAllData();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.local_offer_outlined),
|
||||
label: const Text('編輯標籤'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.teal, // 背景顏色設定為藍綠色
|
||||
foregroundColor: Colors.white, // 文字與圖示顏色設定為白色
|
||||
padding: const EdgeInsets.symmetric(vertical: 12), // 設定垂直內距
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 保持與拍照按鈕一致的圓角
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user