394 lines
13 KiB
Dart
394 lines
13 KiB
Dart
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), // 保持與拍照按鈕一致的圓角
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |