Files
jinzhi/lib/features/assets/presentation/asset_edit_page.dart
T
2026-06-23 10:18:36 +08:00

422 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.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';
class AssetEditPage extends ConsumerStatefulWidget {
const AssetEditPage({super.key, required this.id});
final String id;
@override
ConsumerState<AssetEditPage> createState() => _AssetEditPageState();
}
class _AssetEditPageState extends ConsumerState<AssetEditPage> {
late GoldAssetHolding _holding;
late final TextEditingController _weightCtrl;
late final TextEditingController _costCtrl;
late final TextEditingController _dateCtrl;
late final TextEditingController _channelCtrl;
double _gram = 0;
@override
void initState() {
super.initState();
_holding = ref
.read(assetPortfolioControllerProvider)
.holdings
.map((item) => item.holding)
.firstWhere((item) => item.id == widget.id);
_gram = _holding.weightGram;
_weightCtrl = TextEditingController(
text: _holding.weightGram.toStringAsFixed(
_holding.weightGram % 1 == 0 ? 0 : 1,
),
);
_costCtrl = TextEditingController(
text: _holding.costAmount?.toStringAsFixed(0) ?? '',
);
_dateCtrl = TextEditingController(
text: _holding.purchaseDate == null
? ''
: '${_holding.purchaseDate!.year}-${_holding.purchaseDate!.month.toString().padLeft(2, '0')}-${_holding.purchaseDate!.day.toString().padLeft(2, '0')}',
);
_channelCtrl = TextEditingController(text: _holding.channel ?? '');
}
Future<void> pickDate(BuildContext context) async {
final today = DateTime.now();
final parsed = _parseDate(_dateCtrl.text.trim()) ?? _holding.purchaseDate;
final initialDate = parsed == null
? today
: 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);
});
}
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);
}
@override
void dispose() {
_weightCtrl.dispose();
_costCtrl.dispose();
_dateCtrl.dispose();
_channelCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final summary = ref.watch(assetPortfolioControllerProvider);
final valuation = summary.holdings.firstWhere(
(item) => item.holding.id == widget.id,
);
_holding = valuation.holding;
final cs = ShadTheme.of(context).colorScheme;
final market = ref.watch(marketControllerProvider);
final quote = market.quotes.firstWhere(
(item) => item.metal == _holding.metal,
);
final value = _gram * _holding.purity * quote.spotPrice;
return Scaffold(
backgroundColor: const Color(0xFFFBFAF7),
appBar: AppBar(
backgroundColor: const Color(0xFFFBFAF7),
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
title: Text(
'编辑·${_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: [
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;
_weightCtrl.text = value.toStringAsFixed(
value % 1 == 0 ? 0 : 1,
);
}),
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: 16),
Row(
children: [
Expanded(
child: FilledButton(
onPressed: () {
final grams = double.tryParse(
_weightCtrl.text.trim(),
);
final cost = double.tryParse(_costCtrl.text.trim());
final parsedDate = DateTime.tryParse(
_dateCtrl.text.trim(),
);
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: grams ?? _holding.weightGram,
images: _holding.images,
costAmount: cost,
purchaseDate: parsedDate,
channel: _channelCtrl.text.trim().isEmpty
? null
: _channelCtrl.text.trim(),
createdAt: _holding.createdAt,
updatedAt: DateTime.now(),
status: _holding.status,
),
);
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已保存修改 · 总值已重算')),
);
},
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),
],
),
);
}
}
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),
),
),
),
);
}
}