point record

This commit is contained in:
Phuoc Nguyen
2025-11-26 11:48:02 +07:00
parent 3741239d83
commit a07f165f0c
12 changed files with 1761 additions and 12 deletions

View File

@@ -0,0 +1,654 @@
/// Model House Detail Page
///
/// Displays 360° view launcher, project information, and image gallery.
library;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Model House Detail Page
class ModelHouseDetailPage extends ConsumerWidget {
final String modelId;
const ModelHouseDetailPage({
required this.modelId,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Mock data - in real app, fetch from provider
final modelData = _getMockData(modelId);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text(
'Chi tiết Nhà mẫu',
style: TextStyle(color: Colors.black),
),
actions: [
IconButton(
icon: const FaIcon(
FontAwesomeIcons.shareNodes,
color: Colors.black,
size: 20,
),
onPressed: () => _shareModel(context, modelData),
),
const SizedBox(width: AppSpacing.sm),
],
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
centerTitle: false,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 360° View Launcher
_build360ViewLauncher(context, modelData),
const SizedBox(height: 16),
// Project Information
_buildProjectInfo(modelData),
const SizedBox(height: 16),
// Image Gallery
_buildImageGallery(context, modelData),
const SizedBox(height: 40),
],
),
),
);
}
Widget _build360ViewLauncher(
BuildContext context,
Map<String, dynamic> modelData,
) {
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _launch360View(context, modelData['url360'] as String),
borderRadius: BorderRadius.circular(12),
child: Container(
height: 400,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF667eea), Color(0xFF764ba2)],
),
borderRadius: BorderRadius.circular(12),
),
child: Stack(
children: [
// Background image with overlay
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Opacity(
opacity: 0.3,
child: CachedNetworkImage(
imageUrl: (modelData['images'] as List<Map<String, String>>)
.first['url']!,
fit: BoxFit.cover,
),
),
),
),
// Content
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 360° Icon
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.2),
border: Border.all(color: Colors.white, width: 3),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.arrowsRotate,
size: 40,
color: Colors.white,
),
SizedBox(height: 4),
Text(
'360°',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
const SizedBox(height: 20),
const Text(
'Xem nhà mẫu 360°',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black26,
offset: Offset(0, 2),
blurRadius: 4,
),
],
),
),
const SizedBox(height: 8),
const Text(
'Trải nghiệm không gian thực tế ảo',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
const SizedBox(height: 20),
// Launch Button
Container(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
FaIcon(
FontAwesomeIcons.play,
size: 14,
color: Color(0xFF667eea),
),
SizedBox(width: 8),
Text(
'Bắt đầu tham quan',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF667eea),
),
),
],
),
),
],
),
),
],
),
),
),
),
);
}
Widget _buildProjectInfo(Map<String, dynamic> modelData) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
modelData['title'] as String,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
const SizedBox(height: 16),
// Specs Grid
Row(
children: [
Expanded(
child: _buildSpecItem(
'Diện tích',
modelData['area'] as String,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecItem(
'Địa điểm',
modelData['location'] as String,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecItem(
'Phong cách',
modelData['style'] as String,
),
),
],
),
const SizedBox(height: 20),
// Description
Text(
modelData['description'] as String,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4b5563),
height: 1.6,
),
),
],
),
);
}
Widget _buildSpecItem(String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
label.toUpperCase(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildImageGallery(
BuildContext context,
Map<String, dynamic> modelData,
) {
final images = modelData['images'] as List<Map<String, String>>;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gallery Title
const Row(
children: [
FaIcon(
FontAwesomeIcons.images,
size: 18,
color: AppColors.grey900,
),
SizedBox(width: 8),
Text(
'Thư viện Hình ảnh',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
],
),
const SizedBox(height: 16),
// Gallery Grid
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: images.length,
itemBuilder: (context, index) {
final image = images[index];
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () => _showImageViewer(context, images, index),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 120,
height: 120,
child: CachedNetworkImage(
imageUrl: image['url']!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.grey100,
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
errorWidget: (context, url, error) => Container(
color: AppColors.grey100,
child: const Icon(Icons.error),
),
),
),
),
),
);
},
),
),
],
),
);
}
Future<void> _launch360View(BuildContext context, String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Không thể mở link 360°')),
);
}
}
}
void _showImageViewer(
BuildContext context,
List<Map<String, String>> images,
int initialIndex,
) {
showDialog<void>(
context: context,
barrierColor: Colors.black87,
builder: (context) => _ImageViewerDialog(
images: images,
initialIndex: initialIndex,
),
);
}
void _shareModel(BuildContext context, Map<String, dynamic> modelData) {
Share.share(
'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
subject: modelData['title'] as String,
);
}
Map<String, dynamic> _getMockData(String modelId) {
// Mock data - in real app, fetch from repository
return {
'title': 'Căn hộ Studio',
'area': '35m²',
'location': 'Quận 7',
'style': 'Hiện đại',
'description':
'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.',
'url360': 'https://vr.house3d.com/web/panorama-player/H00179549',
'images': [
{
'url':
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=600&fit=crop',
'caption': 'Phối cảnh tổng thể căn hộ studio với thiết kế hiện đại',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/main_img.jpg',
'caption': 'Khu vực phòng khách với gạch granite cao cấp',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_1.jpg?v=1',
'caption': 'Phòng ngủ chính với gạch ceramic màu trung tính',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_0.jpg?v=1',
'caption': 'Khu vực bếp với gạch mosaic điểm nhấn',
},
{
'url':
'https://images.unsplash.com/photo-1620626011761-996317b8d101?w=800&h=600&fit=crop',
'caption': 'Phòng tắm hiện đại với gạch chống thấm cao cấp',
},
{
'url':
'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_3.jpg?v=1',
'caption': 'Khu vực bàn ăn ấm cúng',
},
],
};
}
}
/// Image Viewer Dialog with Swipe Navigation
class _ImageViewerDialog extends StatefulWidget {
final List<Map<String, String>> images;
final int initialIndex;
const _ImageViewerDialog({
required this.images,
required this.initialIndex,
});
@override
State<_ImageViewerDialog> createState() => _ImageViewerDialogState();
}
class _ImageViewerDialogState extends State<_ImageViewerDialog> {
late PageController _pageController;
late int _currentIndex;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
color: Colors.black,
child: Stack(
children: [
// Main PageView
Center(
child: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.images.length,
itemBuilder: (context, index) {
return Center(
child: CachedNetworkImage(
imageUrl: widget.images[index]['url']!,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.white,
size: 48,
),
),
);
},
),
),
// Top bar with counter and close button
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_currentIndex + 1} / ${widget.images.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
),
),
),
// Caption at bottom
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
),
),
child: Text(
widget.images[_currentIndex]['caption']!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
);
}
}

View File

@@ -169,6 +169,7 @@ class _LibraryTab extends StatelessWidget {
padding: const EdgeInsets.all(20),
children: const [
_LibraryCard(
modelId: 'studio-01',
imageUrl:
'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
title: 'Căn hộ Studio',
@@ -178,6 +179,7 @@ class _LibraryTab extends StatelessWidget {
has360View: true,
),
_LibraryCard(
modelId: 'villa-01',
imageUrl:
'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
title: 'Biệt thự Hiện đại',
@@ -187,6 +189,7 @@ class _LibraryTab extends StatelessWidget {
has360View: true,
),
_LibraryCard(
modelId: 'townhouse-01',
imageUrl:
'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
title: 'Nhà phố Tối giản',
@@ -196,6 +199,7 @@ class _LibraryTab extends StatelessWidget {
has360View: true,
),
_LibraryCard(
modelId: 'apartment-01',
imageUrl:
'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
title: 'Chung cư Cao cấp',
@@ -212,6 +216,7 @@ class _LibraryTab extends StatelessWidget {
/// Library Card Widget
class _LibraryCard extends StatelessWidget {
const _LibraryCard({
required this.modelId,
required this.imageUrl,
required this.title,
required this.date,
@@ -219,6 +224,7 @@ class _LibraryCard extends StatelessWidget {
this.has360View = false,
});
final String modelId;
final String imageUrl;
final String title;
final String date;
@@ -233,13 +239,7 @@ class _LibraryCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 20),
child: InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Chức năng xem chi tiết sẽ được triển khai trong phiên bản tiếp theo',
),
),
);
context.push('/model-houses/$modelId');
},
borderRadius: BorderRadius.circular(12),
child: Column(