import 'dart:io'; import 'package:flutter/material.dart' hide AssetImage; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:uuid/uuid.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import '../../../common/domain/metal_type.dart'; import '../../../common/domain/money.dart'; import '../../../common/widgets/prototype_ui.dart'; import '../../market/application/market_controller.dart'; import '../application/asset_portfolio_controller.dart'; import '../data/asset_models.dart'; enum AssetFormMode { create, edit } class AssetFormPage extends ConsumerStatefulWidget { const AssetFormPage.create({super.key}) : mode = AssetFormMode.create, holding = null; const AssetFormPage.edit({super.key, required this.holding}) : mode = AssetFormMode.edit; final AssetFormMode mode; final GoldAssetHolding? holding; bool get isEdit => mode == AssetFormMode.edit; @override ConsumerState createState() => _AssetFormPageState(); } class _AssetFormPageState extends ConsumerState { static const _cats = [ AssetCategory.bar, AssetCategory.necklace, AssetCategory.ring, AssetCategory.bracelet, AssetCategory.bean, AssetCategory.earring, AssetCategory.silverware, AssetCategory.platinumPiece, ]; static const _metalPurities = { MetalType.gold: ['足金9999', '足金999', '22K', '18K'], MetalType.platinum: ['PT990', 'PT950'], MetalType.silver: ['999银', '925银'], }; final _costCtrl = TextEditingController(); final _noteCtrl = TextEditingController(); final _dateCtrl = TextEditingController(text: '2025-08-12'); final _channelCtrl = TextEditingController(text: '银行'); final _picker = ImagePicker(); final _images = []; final _uuid = const Uuid(); AssetCategory _cat = AssetCategory.bar; MetalType _metal = MetalType.gold; int _purityIndex = 0; double _gram = 50; bool _optionalExpanded = false; static const _maxImages = 9; GoldAssetHolding? get _holding => widget.holding; bool get _isEdit => widget.isEdit; bool get _isImageLimitReached => _images.length >= _maxImages; String get _imageLimitText => '最多可添加 $_maxImages 张图片'; @override void initState() { super.initState(); final holding = _holding; if (holding != null) { _optionalExpanded = true; _cat = holding.category; _metal = holding.metal; _purityIndex = _purityIndexFor(holding); _gram = holding.weightGram; _costCtrl.text = holding.costAmount?.toStringAsFixed(0) ?? ''; _dateCtrl.text = holding.purchaseDate == null ? '' : _formatDate(holding.purchaseDate!); _channelCtrl.text = holding.channel ?? ''; _noteCtrl.text = holding.note ?? ''; _images.addAll(holding.images); } } @override void dispose() { _costCtrl.dispose(); _noteCtrl.dispose(); _dateCtrl.dispose(); _channelCtrl.dispose(); super.dispose(); } int _purityIndexFor(GoldAssetHolding holding) { final options = _metalPurities[holding.metal]!; final index = options.indexOf(holding.purityLabel); return index < 0 ? 0 : index; } List get _purityOptions => _metalPurities[_metal]!; String get _purity => _purityOptions[_purityIndex.clamp(0, _purityOptions.length - 1)]; double _factorFor(String purity) { return switch (purity) { '足金9999' => 0.9999, '足金999' => 0.999, '22K' => 0.916, '18K' => 0.75, 'PT990' => 0.99, 'PT950' => 0.95, '999银' => 0.999, '925银' => 0.925, _ => 1, }; } String _nameForSelection() { return switch (_cat) { AssetCategory.bar => '投资金条', AssetCategory.necklace => '${_metal.label}项链', AssetCategory.ring => '${_metal.label}戒指', AssetCategory.bracelet => '${_metal.label}手镯', AssetCategory.bean => '金豆', AssetCategory.earring => '${_metal.label}耳环', AssetCategory.silverware => '银饰', AssetCategory.platinumPiece => 'PT饰品', }; } String _formatDate(DateTime value) { return '${value.year}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}'; } DateTime? _parseDate(String value) { if (value.isEmpty) return null; return DateTime.tryParse(value); } Future pickDate(BuildContext context) async { final today = DateTime.now(); final parsed = _parseDate(_dateCtrl.text.trim()) ?? _holding?.purchaseDate ?? today; final initialDate = parsed.isBefore(DateTime(2000)) ? DateTime(2000) : parsed.isAfter(today) ? today : parsed; final picked = await showDatePicker( context: context, initialDate: initialDate, firstDate: DateTime(2000), lastDate: today, initialEntryMode: DatePickerEntryMode.calendarOnly, locale: const Locale('zh'), ); if (picked == null || !mounted) return; setState(() { _dateCtrl.text = _formatDate(picked); }); } Future _showImageSourceSheet() async { if (!mounted) return; showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(18)), ), builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 38, height: 4, decoration: BoxDecoration( color: const Color(0xFFE3DFD4), borderRadius: BorderRadius.circular(999), ), ), const SizedBox(height: 14), ListTile( leading: const Icon(Icons.camera_alt_outlined), title: const Text('拍照'), onTap: () { Navigator.of(ctx).pop(); _handleTakePhoto(); }, ), ListTile( leading: const Icon(Icons.photo_library_outlined), title: const Text('从相册选择'), onTap: () { Navigator.of(ctx).pop(); _handlePickFromGallery(); }, ), const SizedBox(height: 8), ListTile( title: const Text( '取消', textAlign: TextAlign.center, style: TextStyle(color: Color(0xFF8A8780)), ), onTap: () => Navigator.of(ctx).pop(), ), ], ), ), ); } Future _handleTakePhoto() async { final granted = await _requestCameraPermission(); if (!granted) { _showPermissionDeniedDialog('相机'); return; } final file = await _picker.pickImage( source: ImageSource.camera, imageQuality: 80, maxWidth: 2048, ); if (file == null) return; await _addPickedFile(file); } Future _handlePickFromGallery() async { final availableSlots = _maxImages - _images.length; if (availableSlots <= 0) return; final assets = await AssetPicker.pickAssets( context, pickerConfig: AssetPickerConfig( maxAssets: availableSlots, requestType: RequestType.image, themeColor: const Color(0xFFB5862A), ), ); if (assets == null || assets.isEmpty) return; final pickedImages = []; for (final asset in assets.take(availableSlots)) { final file = await asset.file; if (file == null) continue; pickedImages.add(await _assetImageFromSourceFile(file)); } if (pickedImages.isEmpty || !mounted) return; setState(() => _images.addAll(pickedImages)); } Future _addPickedFile(XFile file) async { if (_images.length >= _maxImages) return; final image = await _assetImageFromXFile(file); if (!mounted) return; setState(() => _images.add(image)); } Future _assetImageFromXFile(XFile file) { return _assetImageFromSourceFile(File(file.path)); } Future _assetImageFromSourceFile(File sourceFile) async { final localId = 'img${DateTime.now().microsecondsSinceEpoch}${_uuid.v4().split('-').first}'; final extension = _extensionForPath(sourceFile.path); final directory = await getApplicationSupportDirectory(); final imageDirectory = Directory('${directory.path}/asset_images'); if (!await imageDirectory.exists()) { await imageDirectory.create(recursive: true); } final copiedFile = await sourceFile.copy( '${imageDirectory.path}/$localId$extension', ); return AssetImage( localId: localId, localPath: copiedFile.path, createdAt: DateTime.now(), ); } String _extensionForPath(String path) { final fileName = path.split(Platform.pathSeparator).last; final dotIndex = fileName.lastIndexOf('.'); if (dotIndex <= 0 || dotIndex == fileName.length - 1) return '.jpg'; return fileName.substring(dotIndex); } Future _requestCameraPermission() async { final status = await Permission.camera.status; if (status.isGranted) return true; final result = await Permission.camera.request(); return result.isGranted; } void _showPermissionDeniedDialog(String target) { if (!mounted) return; showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('权限不足'), content: Text('无法访问$target,请在系统设置中开启权限。'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消'), ), TextButton( onPressed: () { Navigator.of(ctx).pop(); openAppSettings(); }, child: const Text('去设置'), ), ], ), ); } void _removeImage(String localId) { setState(() => _images.removeWhere((item) => item.localId == localId)); } void _save() { final cost = double.tryParse(_costCtrl.text.trim()); final parsedDate = DateTime.tryParse(_dateCtrl.text.trim()); final channel = _channelCtrl.text.trim(); final note = _noteCtrl.text.trim(); final messenger = ScaffoldMessenger.maybeOf(context); if (_isEdit) { final holding = _holding!; ref .read(assetPortfolioControllerProvider.notifier) .updateHolding( GoldAssetHolding( id: holding.id, name: holding.name, metal: holding.metal, category: holding.category, purity: holding.purity, purityLabel: holding.purityLabel, weightGram: _gram, images: List.unmodifiable(_images), costAmount: cost, purchaseDate: parsedDate, channel: channel.isEmpty ? null : channel, note: note.isEmpty ? null : note, createdAt: holding.createdAt, updatedAt: DateTime.now(), status: holding.status, ), ); messenger?.showSnackBar( const SnackBar(content: Text('已保存修改 · 总值已重算')), ); Navigator.of(context).pop(); return; } final holding = GoldAssetHolding( id: 'h${DateTime.now().microsecondsSinceEpoch}', name: _nameForSelection(), metal: _metal, category: _cat, purity: _factorFor(_purity), purityLabel: _purity, weightGram: _gram, costAmount: cost, purchaseDate: parsedDate, channel: channel.isEmpty ? null : channel, note: note.isEmpty ? null : note, images: List.unmodifiable(_images), createdAt: DateTime.now(), updatedAt: DateTime.now(), ); ref.read(assetPortfolioControllerProvider.notifier).addHolding(holding); messenger?.showSnackBar( const SnackBar(content: Text('已添加 · 总估值已重算')), ); Navigator.of(context).pop(); } Widget _buildImageTile(AssetImage image) { return Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(image.localPath), width: 84, height: 84, fit: BoxFit.cover, ), ), Positioned( top: 6, right: 6, child: GestureDetector( onTap: () => _removeImage(image.localId), child: Container( width: 22, height: 22, decoration: const BoxDecoration( color: Color(0xAA000000), shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 14, color: Colors.white), ), ), ), ], ); } Widget _buildImagesSection() { return Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFECEAE3), width: 0.5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_images.isEmpty) ...[ GestureDetector( onTap: _showImageSourceSheet, child: Row( children: [ Container( width: 60, height: 60, decoration: BoxDecoration( borderRadius: BorderRadius.circular(11), border: Border.all( color: const Color(0xFFD3CDBE), width: 0.5, ), ), child: const Icon( Icons.camera_alt_outlined, color: Color(0xFFB3AFA5), ), ), const SizedBox(width: 10), const Expanded( child: Text( '添加发票 / 实物照片(选填,仅存本机)', style: TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), ), ), ], ), ), const SizedBox(height: 8), const Text( '支持发票、收据、金条证书、实物照片。', style: TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), ), ] else ...[ const Text( '凭证照片', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ for (final image in _images) _buildImageTile(image), if (!_isImageLimitReached) GestureDetector( onTap: _showImageSourceSheet, child: Container( width: 84, height: 84, decoration: BoxDecoration( color: const Color(0xFFF6ECD6), borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.add, color: Color(0xFFB5862A)), SizedBox(height: 5), Text( '继续补充', textAlign: TextAlign.center, style: TextStyle( fontSize: 11, color: Color(0xFFB5862A), ), ), ], ), ), ), if (_isImageLimitReached) Container( width: 84, height: 84, decoration: BoxDecoration( color: const Color(0xFFF2F0EA), borderRadius: BorderRadius.circular(12), border: Border.all( color: const Color(0xFFE3DFD4), width: 0.5, ), ), alignment: Alignment.center, child: const Icon( Icons.check_circle_outline, color: Color(0xFFB3AFA5), ), ), ], ), const SizedBox(height: 8), Text( _imageLimitText, style: const TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), ), ], ], ), ); } Widget _buildCreateBody(BuildContext context, ShadColorScheme cs) { final market = ref.watch(marketControllerProvider); final quote = market.quotes.firstWhere((item) => item.metal == _metal); final value = _gram * _factorFor(_purity) * quote.spotPrice; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text('选择品类', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780))), const SizedBox(height: 10), Wrap( spacing: 9, runSpacing: 9, children: [ for (final cat in _cats) _Chip( label: cat.label, selected: cat == _cat, onTap: () => setState(() { _cat = cat; if (cat == AssetCategory.silverware) { _metal = MetalType.silver; } else if (cat == AssetCategory.platinumPiece) { _metal = MetalType.platinum; } else { _metal = MetalType.gold; } _purityIndex = 0; }), ), ], ), const SizedBox(height: 16), Container( padding: const EdgeInsets.fromLTRB(16, 13, 16, 13), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE3D6B4), width: 0.5), borderRadius: BorderRadius.circular(13), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '金属 / 纯度', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), ), GestureDetector( onTap: () => setState(() { _purityIndex = (_purityIndex + 1) % _purityOptions.length; }), child: Row( children: [ Text( '${_metal.label} · $_purity', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 7), const Icon( Icons.unfold_more, size: 16, color: Color(0xFFB3AFA5), ), ], ), ), ], ), ), const SizedBox(height: 16), const Text('克重(克)', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780))), const SizedBox(height: 9), Row( children: [ _StepButton( icon: Icons.remove, onTap: () => setState(() => _gram = (_gram - 1).clamp(1, 200)), ), const SizedBox(width: 12), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE3D6B4), width: 0.5), borderRadius: BorderRadius.circular(13), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( _gram.toStringAsFixed(_gram % 1 == 0 ? 0 : 1), style: const TextStyle( fontSize: 32, fontWeight: FontWeight.w600, height: 1, ), ), const SizedBox(width: 6), const Padding( padding: EdgeInsets.only(bottom: 2), child: Text( '克', style: TextStyle(fontSize: 15, color: Color(0xFF8A8780)), ), ), ], ), ), ), const SizedBox(width: 12), _StepButton( icon: Icons.add, onTap: () => setState(() => _gram = (_gram + 1).clamp(1, 200)), ), ], ), Slider( min: 1, max: 200, value: _gram.clamp(1, 200), onChanged: (value) => setState(() => _gram = value), activeColor: const Color(0xFFB5862A), inactiveColor: const Color(0xFFECEAE3), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.fromLTRB(16, 15, 16, 16), decoration: BoxDecoration( color: const Color(0xFFF6ECD6), borderRadius: BorderRadius.circular(14), ), child: Row( children: [ const Expanded( child: Text( '实时材料价值', style: TextStyle(fontSize: 13, color: Color(0xFF7A5B12)), ), ), Text( formatCny(value), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600), ), ], ), ), const SizedBox(height: 14), GestureDetector( onTap: () => setState(() => _optionalExpanded = !_optionalExpanded), child: Container( padding: const EdgeInsets.fromLTRB(16, 13, 16, 13), decoration: BoxDecoration( color: cs.card, borderRadius: BorderRadius.circular(14), border: Border.all(color: cs.border, width: 0.5), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '选填:买入成本 · 日期 · 渠道 · 备注 · 照片', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), Icon( _optionalExpanded ? Icons.expand_less : Icons.expand_more, color: cs.mutedForeground, size: 18, ), ], ), ), ), if (_optionalExpanded) ...[ const SizedBox(height: 12), _FieldRow( label: '买入成本(含工费)', child: TextField( controller: _costCtrl, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), ], decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: '选填', ), textAlign: TextAlign.right, ), ), const SizedBox(height: 10), _FieldRow( label: '购买日期', child: TextField( controller: _dateCtrl, readOnly: true, onTap: () => pickDate(context), decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: 'YYYY-MM-DD', ), textAlign: TextAlign.right, ), ), const SizedBox(height: 10), _FieldRow( label: '购买渠道', child: Wrap( spacing: 7, runSpacing: 7, alignment: WrapAlignment.end, children: [ for (final channel in ['银行', '金店', '电商', '回收市场', '其他']) _Chip( label: channel, selected: _channelCtrl.text == channel, onTap: () => setState(() => _channelCtrl.text = channel), ), ], ), ), const SizedBox(height: 10), _FieldRow( label: '备注', child: TextField( controller: _noteCtrl, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: '点击填写', ), textAlign: TextAlign.right, ), ), const SizedBox(height: 10), _buildImagesSection(), ], const SizedBox(height: 16), ], ); } Widget _buildEditBody(BuildContext context, ShadColorScheme cs) { final holding = _holding!; final market = ref.watch(marketControllerProvider); final quote = market.quotes.firstWhere((item) => item.metal == holding.metal); final value = _gram * holding.purity * quote.spotPrice; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ PrototypeCard( child: Row( children: [ Text( '金属 / 纯度', style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), const Spacer(), Text( '${holding.metal.label} · ${holding.purityLabel}', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), ], ), ), const SizedBox(height: 16), const Text('克重(克)', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780))), const SizedBox(height: 9), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE3D6B4), width: 0.5), borderRadius: BorderRadius.circular(13), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( _gram.toStringAsFixed(_gram % 1 == 0 ? 0 : 1), style: const TextStyle( fontSize: 32, fontWeight: FontWeight.w600, height: 1, ), ), const SizedBox(width: 6), const Padding( padding: EdgeInsets.only(bottom: 2), child: Text( '克', style: TextStyle(fontSize: 15, color: Color(0xFF8A8780)), ), ), ], ), ), Slider( min: 1, max: 200, value: _gram.clamp(1, 200), onChanged: (value) => setState(() => _gram = value), activeColor: const Color(0xFFB5862A), inactiveColor: const Color(0xFFECEAE3), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.fromLTRB(16, 15, 16, 16), decoration: BoxDecoration( color: const Color(0xFFF6ECD6), borderRadius: BorderRadius.circular(14), ), child: Row( children: [ const Expanded( child: Text( '实时材料价值', style: TextStyle(fontSize: 13, color: Color(0xFF7A5B12)), ), ), Text( formatCny(value), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600), ), ], ), ), const SizedBox(height: 14), _FieldRow( label: '买入成本(含工费)', child: TextField( controller: _costCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), ], textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: '选填', ), ), ), const SizedBox(height: 10), _FieldRow( label: '购买日期', child: TextField( controller: _dateCtrl, readOnly: true, onTap: () => pickDate(context), textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: 'YYYY-MM-DD', ), ), ), const SizedBox(height: 10), _FieldRow( label: '购买渠道', child: Wrap( spacing: 7, runSpacing: 7, alignment: WrapAlignment.end, children: [ for (final channel in ['银行', '金店', '电商', '回收市场', '其他']) _Chip( label: channel, selected: _channelCtrl.text == channel, onTap: () => setState(() => _channelCtrl.text = channel), ), ], ), ), const SizedBox(height: 10), _FieldRow( label: '备注', child: TextField( controller: _noteCtrl, textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: '点击填写', ), ), ), const SizedBox(height: 10), _buildImagesSection(), const SizedBox(height: 16), ], ); } @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Scaffold( backgroundColor: const Color(0xFFFBFAF7), appBar: AppBar( backgroundColor: const Color(0xFFFBFAF7), surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, title: Text( _isEdit ? '编辑·${_holding!.name}' : '添加资产', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), ), leading: IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.chevron_left, size: 20), ), ), body: SafeArea( top: false, child: ListView( padding: const EdgeInsets.fromLTRB(0, 8, 0, 24), children: [ PrototypeFrame( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (_isEdit) _buildEditBody(context, cs) else _buildCreateBody(context, cs), Row( children: [ Expanded( child: FilledButton( onPressed: _save, child: Text(_isEdit ? '保存修改' : '保存'), ), ), ], ), const SizedBox(height: 10), Text( _isEdit ? '修改后会即时重算资产总值' : '添加后会即时重算资产总值', textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: cs.mutedForeground), ), ], ), ), ], ), ), ); } } class _Chip extends StatelessWidget { const _Chip({ required this.label, required this.selected, required this.onTap, }); final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 6), decoration: BoxDecoration( color: selected ? const Color(0xFFF6ECD6) : Colors.white, borderRadius: BorderRadius.circular(999), border: Border.all( color: selected ? const Color(0xFFE3D6B4) : const Color(0xFFECEAE3), width: 0.5, ), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, color: selected ? const Color(0xFF7A5B12) : const Color(0xFF5F5E5A), ), ), ), ); } } class _StepButton extends StatelessWidget { const _StepButton({required this.icon, required this.onTap}); final IconData icon; final VoidCallback onTap; @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 34, height: 34, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE3D6B4), width: 0.5), ), child: Icon(icon, size: 18, color: const Color(0xFFB5862A)), ), ); } } class _FieldRow extends StatelessWidget { const _FieldRow({required this.label, required this.child}); final String label; final Widget child; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFECEAE3), width: 0.5), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( label, style: const TextStyle(fontSize: 13, color: Color(0xFF8A8780)), ), const SizedBox(width: 10), Expanded(child: child), ], ), ); } }