import 'dart:io'; import 'package:flutter/material.dart' hide AssetImage; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../common/domain/money.dart'; import '../../../common/router/app_router.dart'; import '../../../common/widgets/prototype_ui.dart'; import '../../profile/application/price_color_theme.dart'; import '../../profile/application/profile_settings_controller.dart'; import '../application/asset_portfolio_controller.dart'; import '../data/asset_models.dart'; import 'asset_image_preview_page.dart'; class HoldingDetailPage extends ConsumerWidget { const HoldingDetailPage({super.key, this.id}); final String? id; @override Widget build(BuildContext context, WidgetRef ref) { final summary = ref.watch(assetPortfolioControllerProvider); final holding = summary.holdings .map((item) => item.holding) .firstWhere( (item) => id == null || item.id == id, orElse: () => summary.holdings.first.holding, ); final valuation = summary.holdings.firstWhere( (item) => item.holding.id == holding.id, ); final cs = ShadTheme.of(context).colorScheme; final palette = priceColorPalette( ref.watch(profileSettingsControllerProvider).priceColorMode, ); final changeUp = valuation.todayChange >= 0; return Scaffold( backgroundColor: const Color(0xFFFBFAF7), appBar: AppBar( backgroundColor: const Color(0xFFFBFAF7), surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, leading: IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.chevron_left, size: 20), ), title: Text( holding.name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), ), actions: [ IconButton( onPressed: () => _showActionsSheet(context, ref, holding), icon: const Icon(Icons.more_horiz, size: 22), color: const Color(0xFF8A8780), ), ], ), body: SafeArea( child: ListView( padding: const EdgeInsets.fromLTRB(0, 8, 0, 28), children: [ PrototypeFrame( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 4), PrototypeCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: holding.metal.color.withValues( alpha: 0.15, ), borderRadius: BorderRadius.circular(10), ), child: Icon( Icons.circle_outlined, color: holding.metal.color, size: 18, ), ), const SizedBox(width: 10), Text( '${holding.purityLabel} · ${holding.weightGram.toStringAsFixed(1)}g', style: TextStyle( fontSize: 12, color: cs.mutedForeground, ), ), ], ), const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '¥', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 2), Text( formatCny( valuation.materialValue, ).replaceFirst('¥', ''), style: const TextStyle( fontSize: 42, fontWeight: FontWeight.w600, letterSpacing: -1, height: 1, ), ), ], ), const SizedBox(height: 10), Row( children: [ _ChangeChip( amount: valuation.todayChange, positive: changeUp, upColor: Color(palette.up), downColor: Color(palette.down), upBackground: Color(palette.upBackground), downBackground: Color(palette.downBackground), ), const SizedBox(width: 8), Text( '今日', style: TextStyle( fontSize: 12, color: cs.mutedForeground, ), ), ], ), ], ), ), const SizedBox(height: 12), GestureDetector( onTap: () => context.push( '${AppRoutes.marketKline}?metal=${holding.metal.id}&period=24h', ), child: PrototypeCard( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), child: SizedBox( height: 92, child: ClipRRect( borderRadius: BorderRadius.circular(10), child: PrototypeLineChart( values: const [ 18, 20, 21, 23, 22, 25, 29, 31, 30, 33, 35, 37, ], ), ), ), ), ), const SizedBox(height: 12), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ _InfoRow( label: '材料价值(现货)', value: formatCny(valuation.materialValue), ), const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow( label: '回收参考价', value: formatCny(valuation.materialValue * 0.97), ), if (holding.costAmount != null) ...[ const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow( label: '买入成本', value: formatCny(holding.costAmount!), ), const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow( label: '盈亏', value: _signedCny( valuation.materialValue - holding.costAmount!, ), valueColor: valuation.materialValue - holding.costAmount! >= 0 ? Color(palette.up) : Color(palette.down), ), ], ], ), ), const SizedBox(height: 12), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ _InfoRow( label: '金属 / 纯度', value: '${holding.metal.label} · ${holding.purityLabel}', ), const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow( label: '克重', value: '${holding.weightGram.toStringAsFixed(1)} g', ), const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow( label: '购买日期', value: _dateLabel(holding.purchaseDate), ), const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow(label: '购买渠道', value: holding.channel ?? '未填'), const PrototypeDivider(margin: EdgeInsets.zero), _InfoRow( label: '备注', value: holding.note == null || holding.note!.trim().isEmpty ? '未填' : holding.note!.trim(), ), ], ), ), const SizedBox(height: 12), _buildImageSection(context, holding), const SizedBox(height: 12), Row( children: [ Expanded( child: OutlinedButton( onPressed: () => context.push( '${AppRoutes.assetEdit}?id=${holding.id}', ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFFE3D6B4)), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), foregroundColor: const Color(0xFF9A7A2A), ), child: const Text('编辑'), ), ), const SizedBox(width: 10), Expanded( child: OutlinedButton( onPressed: () => _markSold(context, ref, holding.id), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFFE6E1D8)), foregroundColor: const Color(0xFF8A8780), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), ), child: const Text('标记卖出 / 转赠'), ), ), ], ), ], ), ), ], ), ), ); } void _showActionsSheet( BuildContext context, WidgetRef ref, GoldAssetHolding holding, ) { showModalBottomSheet( context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(22)), ), builder: (ctx) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: const Color(0xFFE3DFD4), borderRadius: BorderRadius.circular(999), ), ), ), _ActionMenuTile( icon: Icons.edit_outlined, label: '编辑持仓', onTap: () { Navigator.of(ctx).pop(); context.push('${AppRoutes.assetEdit}?id=${holding.id}'); }, ), const PrototypeDivider(margin: EdgeInsets.zero), _ActionMenuTile( icon: Icons.share_outlined, label: '分享这张的估值卡', onTap: () { Navigator.of(ctx).pop(); context.push(AppRoutes.profileShare); }, ), const PrototypeDivider(margin: EdgeInsets.zero), _ActionMenuTile( icon: Icons.sell_outlined, label: '标记卖出 / 转赠', labelColor: const Color(0xFFC0392B), iconColor: const Color(0xFFC0392B), onTap: () { Navigator.of(ctx).pop(); _markSold(context, ref, holding.id); }, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消'), ), ), ], ), ), ), ); } void _markSold(BuildContext context, WidgetRef ref, String id) { ref.read(assetPortfolioControllerProvider.notifier).markHoldingSold(id); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('已标记卖出 / 转赠'))); Navigator.of(context).pop(); } Widget _buildImageSection( BuildContext context, GoldAssetHolding holding, ) { final images = holding.images; return PrototypeCard( padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '凭证照片', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), ), const SizedBox(height: 12), if (images.isEmpty) const Text( '暂无凭证照片,可在添加资产时上传发票、证书或实物照片。', style: TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), ) else Wrap( spacing: 8, runSpacing: 8, children: [ for (var index = 0; index < images.length; index++) _buildDetailImageTile( context, holding, images[index], index, ), ], ), ], ), ); } Widget _buildDetailImageTile( BuildContext context, GoldAssetHolding holding, AssetImage image, int index, ) { return GestureDetector( onTap: () => _openImagePreview(context, holding.images, index), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( File(image.localPath), width: 84, height: 84, fit: BoxFit.cover, ), ), ); } void _openImagePreview( BuildContext context, List images, int initialIndex, ) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => AssetImagePreviewPage( images: images, initialIndex: initialIndex, ), ), ); } } class _ActionMenuTile extends StatelessWidget { const _ActionMenuTile({ required this.icon, required this.label, required this.onTap, this.iconColor, this.labelColor, }); final IconData icon; final String label; final VoidCallback onTap; final Color? iconColor; final Color? labelColor; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 16), child: Row( children: [ Icon(icon, size: 20, color: iconColor ?? const Color(0xFF8A8780)), const SizedBox(width: 12), Text( label, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: labelColor ?? cs.foreground, ), ), ], ), ), ); } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value, this.valueColor}); final String label; final String value; final Color? valueColor; @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle(fontSize: 13, color: cs.mutedForeground), ), Text( value, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: valueColor ?? cs.foreground, ), ), ], ), ); } } class _ChangeChip extends StatelessWidget { const _ChangeChip({ required this.amount, required this.positive, required this.upColor, required this.downColor, required this.upBackground, required this.downBackground, }); final double amount; final bool positive; final Color upColor; final Color downColor; final Color upBackground; final Color downBackground; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: positive ? upBackground : downBackground, borderRadius: BorderRadius.circular(999), ), child: Text( '${positive ? '↑' : '↓'}${formatCny(amount.abs())} · ${positive ? '+' : '-'}${amount.abs().toStringAsFixed(2)}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: positive ? upColor : downColor, ), ), ); } } 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( children: [ Text( label, style: const TextStyle(fontSize: 13, color: Color(0xFF8A8780)), ), const SizedBox(width: 10), Expanded(child: child), ], ), ); } } String _dateLabel(DateTime? value) { if (value == null) return '未填'; return '${value.year}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}'; } String _signedCny(double value) { final sign = value >= 0 ? '+' : '-'; return '$sign${formatCny(value.abs())}'; }