import 'package:flutter/material.dart'; 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'; 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), ), ), 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 SizedBox(height: 14), Row( children: [ Expanded( child: OutlinedButton( onPressed: () => _showEditSheet(context, ref, holding), child: const Text('编辑'), ), ), const SizedBox(width: 10), Expanded( child: OutlinedButton( onPressed: () => _markSold(context, ref, holding.id), child: const Text('标记卖出 / 转赠'), ), ), ], ), ], ), ), ], ), ), ); } void _showEditSheet( BuildContext context, WidgetRef ref, GoldAssetHolding holding, ) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(22)), ), builder: (ctx) => _EditHoldingSheet( holding: holding, onSave: (updated) { ref .read(assetPortfolioControllerProvider.notifier) .updateHolding(updated); Navigator.of(ctx).pop(); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: 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(); } } 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 _EditHoldingSheet extends StatefulWidget { const _EditHoldingSheet({required this.holding, required this.onSave}); final GoldAssetHolding holding; final ValueChanged onSave; @override State<_EditHoldingSheet> createState() => _EditHoldingSheetState(); } class _EditHoldingSheetState extends State<_EditHoldingSheet> { late final TextEditingController _weightCtrl; late final TextEditingController _costCtrl; late final TextEditingController _dateCtrl; late final TextEditingController _channelCtrl; late final TextEditingController _noteCtrl; @override void initState() { super.initState(); _weightCtrl = TextEditingController( text: widget.holding.weightGram.toStringAsFixed( widget.holding.weightGram % 1 == 0 ? 0 : 1, ), ); _costCtrl = TextEditingController( text: widget.holding.costAmount?.toStringAsFixed(0) ?? '', ); _dateCtrl = TextEditingController( text: widget.holding.purchaseDate == null ? '' : '${widget.holding.purchaseDate!.year}-${widget.holding.purchaseDate!.month.toString().padLeft(2, '0')}-${widget.holding.purchaseDate!.day.toString().padLeft(2, '0')}', ); _channelCtrl = TextEditingController(text: widget.holding.channel ?? ''); _noteCtrl = TextEditingController(text: widget.holding.note ?? ''); } @override void dispose() { _weightCtrl.dispose(); _costCtrl.dispose(); _dateCtrl.dispose(); _channelCtrl.dispose(); _noteCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; return SafeArea( child: Padding( padding: EdgeInsets.only( bottom: MediaQuery.viewInsetsOf(context).bottom, ), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 14), decoration: BoxDecoration( color: const Color(0xFFE3DFD4), borderRadius: BorderRadius.circular(999), ), ), ), Row( children: [ const Spacer(), const Text( '编辑持仓', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const Spacer(), GestureDetector( onTap: () => Navigator.of(context).pop(), child: const Icon( Icons.close, size: 20, color: Color(0xFF8A8780), ), ), ], ), const SizedBox(height: 16), _FieldRow( label: '金属 / 纯度', child: Text( '${widget.holding.metal.label} · ${widget.holding.purityLabel}', textAlign: TextAlign.right, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 10), _FieldRow( label: '克重(克)', child: TextField( controller: _weightCtrl, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, ), ), ), const SizedBox(height: 10), _FieldRow( label: '买入成本(含工费)', child: TextField( controller: _costCtrl, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: '选填', ), ), ), const SizedBox(height: 10), _FieldRow( label: '购买日期', child: TextField( controller: _dateCtrl, textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: 'YYYY-MM-DD', ), ), ), const SizedBox(height: 10), _FieldRow( label: '购买渠道', child: TextField( controller: _channelCtrl, textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, isDense: true, hintText: '选填', ), ), ), 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: 16), SizedBox( width: double.infinity, child: FilledButton( onPressed: () { final grams = double.tryParse(_weightCtrl.text.trim()); final cost = double.tryParse(_costCtrl.text.trim()); final parsedDate = DateTime.tryParse( _dateCtrl.text.trim(), ); widget.onSave( GoldAssetHolding( id: widget.holding.id, name: widget.holding.name, metal: widget.holding.metal, category: widget.holding.category, purity: widget.holding.purity, purityLabel: widget.holding.purityLabel, weightGram: grams ?? widget.holding.weightGram, costAmount: cost, purchaseDate: parsedDate, channel: _channelCtrl.text.trim().isEmpty ? null : _channelCtrl.text.trim(), note: _noteCtrl.text.trim().isEmpty ? null : _noteCtrl.text.trim(), createdAt: widget.holding.createdAt, updatedAt: DateTime.now(), status: widget.holding.status, ), ); }, child: const Text('保存修改'), ), ), const SizedBox(height: 10), Text( '修改后会即时重算资产总值', textAlign: TextAlign.center, style: TextStyle(fontSize: 11, color: cs.mutedForeground), ), ], ), ), ), ), ); } } 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())}'; }