import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../shared/widgets/nav_bar_config.dart'; import '../../core/utils/responsive.dart'; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/action_bar.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors_extension.dart'; class OutingLogCreatePage extends ConsumerStatefulWidget { const OutingLogCreatePage({super.key}); @override ConsumerState createState() => _OutingLogCreatePageState(); } class _OutingLogCreatePageState extends ConsumerState { final _customerCtrl = TextEditingController(); final _summaryCtrl = TextEditingController(); final _planCtrl = TextEditingController(); // GPS 模拟 String _gpsAddress = '深圳市南山区科技园南路88号'; final double _gpsLat = 22.5431; final double _gpsLng = 113.9532; double _gpsAccuracy = 15.0; bool _gpsFailed = false; // 客户联想 final List _mockCustomers = [ '华软科技', '云创数据', '数据引力', '天诚科技', '博思软件', '智云科技', '恒通信息', '创新无限', ]; List _customerSuggestions = []; bool _showCustomerSuggestions = false; String? _selectedCustomer; // 客户联系人 final Map>> _mockContacts = { '华软科技': [ {'name': '赵经理', 'phone': '13800138001', 'position': 'IT经理'}, {'name': '李主管', 'phone': '13800138002', 'position': '采购主管'}, ], '云创数据': [ {'name': '陈经理', 'phone': '13800138003', 'position': '技术总监'}, ], '数据引力': [ {'name': '孙总', 'phone': '13800138004', 'position': '总经理'}, ], '天诚科技': [ {'name': '周主任', 'phone': '13800138005', 'position': '办公室主任'}, ], }; Map? _selectedContact; // 照片 final List _photos = []; static const int _maxPhotos = 9; @override void dispose() { _customerCtrl.dispose(); _summaryCtrl.dispose(); _planCtrl.dispose(); super.dispose(); } void _onCustomerChanged(String value) { setState(() { _selectedCustomer = null; _selectedContact = null; if (value.isEmpty) { _customerSuggestions = []; _showCustomerSuggestions = false; } else { _customerSuggestions = _mockCustomers .where((c) => c.contains(value)) .toList(); _showCustomerSuggestions = _customerSuggestions.isNotEmpty; } }); } void _selectCustomer(String customer) { setState(() { _selectedCustomer = customer; _customerCtrl.text = customer; _showCustomerSuggestions = false; _selectedContact = null; }); } Future _pickContact() async { final l10n = AppLocalizations.of(context); if (_selectedCustomer == null) { TDToast.showText(l10n.get('selectCustomerFirst'), context: context); return; } final contacts = _mockContacts[_selectedCustomer]; if (contacts == null || contacts.isEmpty) { TDToast.showText(l10n.get('noContact'), context: context); return; } final result = await showDialog>( context: context, builder: (ctx) => TDAlertDialog.vertical( title: l10n.get('selectContact'), buttons: contacts .map( (c) => TDDialogButtonOptions( title: '${c['name']} ${c['position']} ${c['phone']}', action: () => Navigator.pop(ctx, c), ), ) .toList(), ), ); if (result != null) { setState(() => _selectedContact = result); } } Future _takePhoto() async { final l10n = AppLocalizations.of(context); if (_photos.length >= _maxPhotos) { TDToast.showText(l10n.get('maxPhotoCount'), context: context); return; } final idx = _photos.length + 1; setState(() { _photos.add('photo_placeholder_$idx'); }); if (context.mounted) { TDToast.showText( l10n.getString( 'mockPhotoTaken', args: { 'idx': '$idx', 'time': DateTime.now().toString().substring(0, 19), 'lat': '$_gpsLat°N', 'lng': '$_gpsLng°E', }, ), context: context, ); } } void _removePhoto(int index) { setState(() => _photos.removeAt(index)); } Future _simulateGps() async { final l10n = AppLocalizations.of(context); setState(() { _gpsFailed = false; _gpsAddress = '深圳市南山区科技园南路88号'; _gpsAccuracy = 15.0; }); TDToast.showText(l10n.get('gpsSuccess'), context: context); } Future _saveDraft() async { final l10n = AppLocalizations.of(context); if (context.mounted) { TDToast.showText(l10n.get('draftSavedToast'), context: context); } } Future _submit() async { final l10n = AppLocalizations.of(context); if (_gpsFailed) { TDToast.showText(l10n.get('gpsPermission'), context: context); return; } if (_gpsAddress.isEmpty) { TDToast.showText(l10n.get('gpsLocatingWait'), context: context); return; } if (_photos.isEmpty) { TDToast.showText(l10n.get('requiredPhotos'), context: context); return; } if (_summaryCtrl.text.trim().isEmpty) { TDToast.showText(l10n.get('requiredSummary'), context: context); return; } if (context.mounted) { TDToast.showText(l10n.get('outingLogSubmitted'), context: context); context.pop(); } } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final r = ResponsiveHelper.of(context); final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('outingLogCreate'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: r.formMaxWidth), child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ _buildGpsSection(), const SizedBox(height: 8), FormSection( title: l10n.get('customerInfo'), children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TDInput( controller: _customerCtrl, hintText: l10n.get('searchCustomer'), onChanged: _onCustomerChanged, ), if (_showCustomerSuggestions) ..._customerSuggestions.map( (s) => GestureDetector( onTap: () => _selectCustomer(s), child: Container( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 4, ), decoration: BoxDecoration( border: Border( top: BorderSide(color: colors.border), ), ), child: Text( s, style: TextStyle( fontSize: 14, color: colors.textPrimary, ), ), ), ), ), ], ), ), const SizedBox(height: 12), GestureDetector( onTap: _pickContact, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 12, ), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _selectedContact != null ? '${_selectedContact!['name']} ${_selectedContact!['phone']}' : l10n.get('selectContactHint'), style: TextStyle( fontSize: 14, color: _selectedContact != null ? colors.textPrimary : colors.textPlaceholder, ), ), Icon( Icons.chevron_right, size: 14, color: colors.textPlaceholder, ), ], ), ), ), ], ), const SizedBox(height: 8), FormSection( title: l10n.get('workSummary'), children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TDInput( controller: _summaryCtrl, maxLines: 5, hintText: l10n.get('workSummaryRequiredHint'), ), ), ], ), const SizedBox(height: 8), FormSection( title: l10n.get('followUp'), children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), ), child: TDInput( controller: _planCtrl, hintText: l10n.get('followUpOptional'), ), ), ], ), const SizedBox(height: 8), FormSection( title: l10n.get('sitePhotos'), actionText: _photos.length >= _maxPhotos ? l10n.get('limitReached') : l10n.get('takePhoto'), showAction: _photos.length < _maxPhotos, actionIcon: Icons.camera_alt_outlined, onActionTap: _photos.length >= _maxPhotos ? null : _takePhoto, children: [ if (_photos.isEmpty) GestureDetector( onTap: _takePhoto, child: Container( height: 100, decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), border: Border.all( color: colors.border, width: 1, ), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt_outlined, size: 32, color: colors.primary, ), SizedBox(height: 4), Text( l10n.get('tapToTakePhoto'), style: TextStyle( fontSize: 12, color: colors.textPlaceholder, ), ), ], ), ), ), ) else Wrap( spacing: 8, runSpacing: 8, children: [ ..._photos.asMap().entries.map( (entry) => _buildPhotoThumbnail( entry.key, entry.value, ), ), if (_photos.length < _maxPhotos) GestureDetector( onTap: _takePhoto, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(4), border: Border.all(color: colors.border), ), child: Icon( Icons.add, size: 28, color: colors.primary, ), ), ), ], ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(4), ), child: Row( children: [ Icon( Icons.info_outline, size: 14, color: colors.primary, ), const SizedBox(width: 6), Expanded( child: Text( l10n.getString( 'watermarkHintDynamic', args: { 'lat': _gpsLat.toStringAsFixed(4), 'lng': _gpsLng.toStringAsFixed(4), }, ), style: TextStyle( fontSize: 11, color: colors.textSecondary, ), ), ), ], ), ), ], ), const SizedBox(height: 80), ], ), ), ), ), ), ActionBar( centerLabel: l10n.get('saveDraft'), rightLabel: l10n.get('submit'), showLeft: false, onCenterTap: _saveDraft, onRightTap: _submit, ), ], ); } Widget _buildGpsSection() { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; if (_gpsFailed) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.location_off, size: 22, color: colors.statusGray), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.get('gpsFailed'), style: TextStyle(fontSize: 14, color: colors.textPrimary), ), SizedBox(height: 4), Text( l10n.get('gpsFailedHint'), style: TextStyle(fontSize: 12, color: colors.textSecondary), ), ], ), ), GestureDetector( onTap: _simulateGps, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(4), ), child: Text( l10n.get('retry'), style: TextStyle(fontSize: 12, color: colors.primary), ), ), ), ], ), ); } final isWarning = _gpsAccuracy > 100; return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.shield_outlined, size: 22, color: isWarning ? colors.warning : colors.success, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _gpsAddress, style: TextStyle(fontSize: 14, color: colors.textPrimary), ), const SizedBox(height: 4), Row( children: [ Text( '${_gpsLat.toStringAsFixed(4)}°N, ${_gpsLng.toStringAsFixed(4)}°E · 精度 ${_gpsAccuracy.toStringAsFixed(0)}m', style: TextStyle( fontSize: 12, color: isWarning ? colors.warning : colors.textSecondary, ), ), if (isWarning) ...[ const SizedBox(width: 6), Icon( Icons.warning_amber_rounded, size: 14, color: colors.warning, ), ], ], ), ], ), ), ], ), ); } Widget _buildPhotoThumbnail(int index, String photo) { final colors = Theme.of(context).extension()!; return Stack( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: colors.infoBorder, borderRadius: BorderRadius.circular(4), ), child: Center( child: Icon(Icons.image_outlined, size: 32, color: colors.primary), ), ), Positioned( top: -4, right: -4, child: GestureDetector( onTap: () => _removePhoto(index), child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: colors.danger, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 14, color: Colors.white), ), ), ), Positioned( bottom: 2, left: 2, right: 2, child: Container( padding: const EdgeInsets.symmetric(horizontal: 2), color: Colors.black54, child: Text( '${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')} ${_gpsLat.toStringAsFixed(2)},${_gpsLng.toStringAsFixed(2)}', style: const TextStyle(fontSize: 8, color: Colors.white), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), ], ); } }