fix:UI对齐demo

This commit is contained in:
jingyun
2026-06-18 18:08:29 +08:00
parent f42ad566e0
commit 2d7523f901
14 changed files with 1613 additions and 355 deletions
+217 -57
View File
@@ -8,9 +8,10 @@ import '../../../common/domain/money.dart';
import '../../../common/router/app_router.dart';
import '../../../common/widgets/prototype_ui.dart';
import '../application/asset_portfolio_controller.dart';
import '../../profile/application/price_color_theme.dart';
import '../../profile/application/profile_settings_controller.dart';
import '../data/asset_models.dart';
import '../../market/application/market_controller.dart';
import '../../profile/application/profile_settings_controller.dart';
class AssetsPage extends ConsumerWidget {
const AssetsPage({super.key});
@@ -19,6 +20,7 @@ class AssetsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final summary = ref.watch(assetPortfolioControllerProvider);
final settings = ref.watch(profileSettingsControllerProvider);
final palette = priceColorPalette(settings.priceColorMode);
final cs = ShadTheme.of(context).colorScheme;
final hidden = settings.amountHidden;
@@ -62,7 +64,9 @@ class AssetsPage extends ConsumerWidget {
.read(profileSettingsControllerProvider.notifier)
.toggleAmountHidden(),
icon: Icon(
hidden ? Icons.visibility_off_outlined : Icons.visibility_outlined,
hidden
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
size: 20,
),
color: const Color(0xFF9A968D),
@@ -97,7 +101,9 @@ class AssetsPage extends ConsumerWidget {
),
const SizedBox(width: 3),
Text(
hidden ? '••••••' : formatCny(summary.totalValue).replaceFirst('¥', ''),
hidden
? '••••••'
: formatCny(summary.totalValue).replaceFirst('¥', ''),
style: const TextStyle(
fontSize: 64,
fontWeight: FontWeight.w600,
@@ -114,6 +120,10 @@ class AssetsPage extends ConsumerWidget {
_ChangeChip(
amount: summary.todayChangeAmount,
percent: summary.todayChangePercent,
upColor: Color(palette.up),
downColor: Color(palette.down),
upBackground: Color(palette.upBackground),
downBackground: Color(palette.downBackground),
),
const SizedBox(width: 10),
Text(
@@ -129,11 +139,13 @@ class AssetsPage extends ConsumerWidget {
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
itemCount: summary.breakdowns.length,
separatorBuilder: (context, index) => const SizedBox(width: 6),
separatorBuilder: (context, index) =>
const SizedBox(width: 6),
itemBuilder: (context, index) {
final item = summary.breakdowns[index];
return PrototypePill(
label: '${item.metal.shortLabel} ${hidden ? '•••' : formatCny(item.value)}',
label:
'${item.metal.shortLabel} ${hidden ? '•••' : formatCny(item.value)}',
leading: Container(
width: 6,
height: 6,
@@ -150,7 +162,10 @@ class AssetsPage extends ConsumerWidget {
Text(
'更新于 16:11 · 行情聚合',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 11, color: cs.mutedForeground.withValues(alpha: 0.82)),
style: TextStyle(
fontSize: 11,
color: cs.mutedForeground.withValues(alpha: 0.82),
),
),
const SizedBox(height: 12),
GestureDetector(
@@ -162,7 +177,20 @@ class AssetsPage extends ConsumerWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: PrototypeLineChart(
values: const [27, 31, 29, 35, 33, 37, 42, 40, 46, 44, 50, 53],
values: const [
27,
31,
29,
35,
33,
37,
42,
40,
46,
44,
50,
53,
],
),
),
),
@@ -176,17 +204,25 @@ class AssetsPage extends ConsumerWidget {
crossAxisSpacing: 10,
childAspectRatio: 1.6,
children: [
_StatCard(title: '回收参考', value: hidden ? '¥••••' : formatCny(summary.recycleReferenceValue)),
_StatCard(
title: '回收参考',
value: hidden
? '¥••••'
: formatCny(summary.recycleReferenceValue),
),
_StatCard(
title: '总盈亏(已填成本)',
value: hidden
? '••••'
: _signedCny(summary.totalValue - summary.totalCost),
valueColor: summary.totalValue - summary.totalCost >= 0
? const Color(0xFFC0392B)
: const Color(0xFF2E8B6F),
? Color(palette.up)
: Color(palette.down),
),
_StatCard(
title: '总成本',
value: hidden ? '¥••••' : formatCny(summary.totalCost),
),
_StatCard(title: '总成本', value: hidden ? '¥••••' : formatCny(summary.totalCost)),
_StatCard(
title: '总克重',
value: '${summary.totalWeightGram.toStringAsFixed(1)} g',
@@ -225,6 +261,8 @@ class AssetsPage extends ConsumerWidget {
_HoldingRow(
valuation: item,
hidden: hidden,
upColor: Color(palette.up),
downColor: Color(palette.down),
onTap: () => context.push(
'${AppRoutes.assetDetail}?id=${item.holding.id}',
),
@@ -285,11 +323,15 @@ class _HoldingRow extends StatelessWidget {
const _HoldingRow({
required this.valuation,
required this.hidden,
required this.upColor,
required this.downColor,
required this.onTap,
});
final HoldingValuation valuation;
final bool hidden;
final Color upColor;
final Color downColor;
final VoidCallback onTap;
@override
@@ -311,7 +353,11 @@ class _HoldingRow extends StatelessWidget {
color: metal.color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(_iconFor(holding.category), size: 18, color: metal.color),
child: Icon(
_iconFor(holding.category),
size: 18,
color: metal.color,
),
),
const SizedBox(width: 12),
Expanded(
@@ -320,13 +366,19 @@ class _HoldingRow extends StatelessWidget {
children: [
Text(
holding.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'${holding.purityLabel} · ${holding.weightGram.toStringAsFixed(1)}g'
'${holding.note == null ? '' : ' · ${holding.note}'}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)),
style: const TextStyle(
fontSize: 12,
color: Color(0xFF8A8780),
),
),
],
),
@@ -337,14 +389,17 @@ class _HoldingRow extends StatelessWidget {
children: [
Text(
hidden ? '¥••••' : formatCny(valuation.materialValue),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'${changeUp ? '' : ''}${hidden ? '¥••••' : formatCny(change.abs())}',
style: TextStyle(
fontSize: 12,
color: changeUp ? const Color(0xFFC0392B) : const Color(0xFF2E8B6F),
color: changeUp ? upColor : downColor,
),
),
],
@@ -369,11 +424,7 @@ class _HoldingRow extends StatelessWidget {
}
class _StatCard extends StatelessWidget {
const _StatCard({
required this.title,
required this.value,
this.valueColor,
});
const _StatCard({required this.title, required this.value, this.valueColor});
final String title;
final String value;
@@ -387,7 +438,10 @@ class _StatCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: TextStyle(fontSize: 12, color: cs.mutedForeground)),
Text(
title,
style: TextStyle(fontSize: 12, color: cs.mutedForeground),
),
Text(
value,
style: TextStyle(
@@ -403,10 +457,21 @@ class _StatCard extends StatelessWidget {
}
class _ChangeChip extends StatelessWidget {
const _ChangeChip({required this.amount, required this.percent});
const _ChangeChip({
required this.amount,
required this.percent,
required this.upColor,
required this.downColor,
required this.upBackground,
required this.downBackground,
});
final double amount;
final double percent;
final Color upColor;
final Color downColor;
final Color upBackground;
final Color downBackground;
@override
Widget build(BuildContext context) {
@@ -414,7 +479,7 @@ class _ChangeChip extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5),
decoration: BoxDecoration(
color: positive ? const Color(0xFFFBEDEA) : const Color(0xFFE7F2EE),
color: positive ? upBackground : downBackground,
borderRadius: BorderRadius.circular(999),
),
child: Text(
@@ -422,7 +487,7 @@ class _ChangeChip extends StatelessWidget {
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: positive ? const Color(0xFFC0392B) : const Color(0xFF2E8B6F),
color: positive ? upColor : downColor,
),
),
);
@@ -491,7 +556,8 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
List<String> get _purityOptions => _metalPurities[_metal]!;
String get _purity => _purityOptions[_purityIndex.clamp(0, _purityOptions.length - 1)];
String get _purity =>
_purityOptions[_purityIndex.clamp(0, _purityOptions.length - 1)];
void _cyclePurity() {
setState(() {
@@ -530,12 +596,15 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final market = ref.watch(marketControllerProvider);
final basePrice = market.quote.spotPrice;
final quote = market.quotes.firstWhere((item) => item.metal == _metal);
final basePrice = quote.spotPrice;
final value = _gram * _factorFor(_purity) * basePrice;
return SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
padding: EdgeInsets.only(
bottom: MediaQuery.viewInsetsOf(context).bottom,
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
@@ -559,17 +628,27 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
const Spacer(),
const Text(
'添加资产',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
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)),
child: const Icon(
Icons.close,
size: 20,
color: Color(0xFF8A8780),
),
),
],
),
const SizedBox(height: 16),
const Text('选择品类', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780))),
const Text(
'选择品类',
style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)),
),
const SizedBox(height: 10),
Wrap(
spacing: 9,
@@ -598,23 +677,39 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
padding: const EdgeInsets.fromLTRB(16, 13, 16, 13),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFE3D6B4), width: 0.5),
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))),
const Text(
'金属 / 纯度',
style: TextStyle(
fontSize: 13,
color: Color(0xFF8A8780),
),
),
GestureDetector(
onTap: _cyclePurity,
child: Row(
children: [
Text(
'${_metal.label} · $_purity',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 7),
const Icon(Icons.unfold_more, size: 16, color: Color(0xFFB3AFA5)),
const Icon(
Icons.unfold_more,
size: 16,
color: Color(0xFFB3AFA5),
),
],
),
),
@@ -622,21 +717,31 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
),
),
const SizedBox(height: 16),
const Text('克重(克)', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780))),
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)),
onTap: () =>
setState(() => _gram = (_gram - 1).clamp(1, 200)),
),
const SizedBox(width: 12),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFE3D6B4), width: 0.5),
border: Border.all(
color: const Color(0xFFE3D6B4),
width: 0.5,
),
borderRadius: BorderRadius.circular(13),
),
child: Row(
@@ -645,12 +750,22 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
children: [
Text(
_gram.toStringAsFixed(_gram % 1 == 0 ? 0 : 1),
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w600, height: 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))),
child: Text(
'',
style: TextStyle(
fontSize: 15,
color: Color(0xFF8A8780),
),
),
),
],
),
@@ -659,7 +774,8 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
const SizedBox(width: 12),
_StepButton(
icon: Icons.add,
onTap: () => setState(() => _gram = (_gram + 1).clamp(1, 200)),
onTap: () =>
setState(() => _gram = (_gram + 1).clamp(1, 200)),
),
],
),
@@ -684,17 +800,29 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('实时材料价值', style: TextStyle(fontSize: 13, color: Color(0xFF7A5B12))),
const Text(
'实时材料价值',
style: TextStyle(
fontSize: 13,
color: Color(0xFF7A5B12),
),
),
Text(
formatCny(value),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 7),
Text(
'${_metal.label}现货 ${basePrice.toStringAsFixed(2)}/克 · 非金店零售价、非回收实收价',
style: const TextStyle(fontSize: 11, color: Color(0xFF9A7A2A)),
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9A7A2A),
),
),
],
),
@@ -714,7 +842,10 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
children: [
Text(
'选填:买入成本 · 日期 · 渠道 · 备注 · 照片',
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
style: TextStyle(
fontSize: 13,
color: cs.mutedForeground,
),
),
Icon(
_expanded ? Icons.expand_less : Icons.expand_more,
@@ -731,7 +862,9 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
label: '买入成本(含工费)',
child: TextField(
controller: _costCtrl,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
@@ -764,7 +897,8 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
_Chip(
label: channel,
selected: _channelCtrl.text == channel,
onTap: () => setState(() => _channelCtrl.text = channel),
onTap: () =>
setState(() => _channelCtrl.text = channel),
),
],
),
@@ -788,7 +922,10 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFECEAE3), width: 0.5),
border: Border.all(
color: const Color(0xFFECEAE3),
width: 0.5,
),
),
child: Row(
children: [
@@ -797,15 +934,24 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(11),
border: Border.all(color: const Color(0xFFD3CDBE), width: 0.5),
border: Border.all(
color: const Color(0xFFD3CDBE),
width: 0.5,
),
),
child: const Icon(
Icons.camera_alt_outlined,
color: Color(0xFFB3AFA5),
),
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)),
style: TextStyle(
fontSize: 12,
color: Color(0xFFB3AFA5),
),
),
),
],
@@ -838,14 +984,25 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
purityLabel: _purity,
weightGram: _gram,
costAmount: double.tryParse(_costCtrl.text.trim()),
purchaseDate: DateTime.tryParse(_dateCtrl.text.trim()),
channel: _channelCtrl.text.trim().isEmpty ? null : _channelCtrl.text.trim(),
note: _noteCtrl.text.trim().isEmpty ? null : _noteCtrl.text.trim(),
purchaseDate: DateTime.tryParse(
_dateCtrl.text.trim(),
),
channel: _channelCtrl.text.trim().isEmpty
? null
: _channelCtrl.text.trim(),
note: _noteCtrl.text.trim().isEmpty
? null
: _noteCtrl.text.trim(),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
ref.read(assetPortfolioControllerProvider.notifier).addHolding(holding);
ref
.read(assetPortfolioControllerProvider.notifier)
.addHolding(holding);
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已添加 · 总估值已重算')),
);
},
child: const Text('保存'),
),
@@ -941,7 +1098,10 @@ class _FieldRow extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF8A8780))),
Text(
label,
style: const TextStyle(fontSize: 13, color: Color(0xFF8A8780)),
),
const SizedBox(width: 10),
Expanded(child: child),
],
@@ -1,10 +1,15 @@
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});
@@ -20,9 +25,13 @@ class HoldingDetailPage extends ConsumerWidget {
(item) => id == null || item.id == id,
orElse: () => summary.holdings.first.holding,
);
final valuation = summary.holdings
.firstWhere((item) => item.holding.id == holding.id);
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(
@@ -61,15 +70,24 @@ class HoldingDetailPage extends ConsumerWidget {
width: 36,
height: 36,
decoration: BoxDecoration(
color: holding.metal.color.withValues(alpha: 0.15),
color: holding.metal.color.withValues(
alpha: 0.15,
),
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.circle_outlined, color: holding.metal.color, size: 18),
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),
style: TextStyle(
fontSize: 12,
color: cs.mutedForeground,
),
),
],
),
@@ -79,11 +97,16 @@ class HoldingDetailPage extends ConsumerWidget {
children: [
const Text(
'¥',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 2),
Text(
formatCny(valuation.materialValue).replaceFirst('¥', ''),
formatCny(
valuation.materialValue,
).replaceFirst('¥', ''),
style: const TextStyle(
fontSize: 42,
fontWeight: FontWeight.w600,
@@ -99,11 +122,18 @@ class HoldingDetailPage extends ConsumerWidget {
_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),
style: TextStyle(
fontSize: 12,
color: cs.mutedForeground,
),
),
],
),
@@ -111,23 +141,67 @@ class HoldingDetailPage extends ConsumerWidget {
),
),
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)),
_InfoRow(
label: '材料价值(现货)',
value: formatCny(valuation.materialValue),
),
const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '回收参考价', value: formatCny(valuation.materialValue * 0.97)),
_InfoRow(
label: '回收参考价',
value: formatCny(valuation.materialValue * 0.97),
),
if (holding.costAmount != null) ...[
const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '买入成本', value: formatCny(holding.costAmount!)),
_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
? const Color(0xFFC0392B)
: const Color(0xFF2E8B6F),
value: _signedCny(
valuation.materialValue - holding.costAmount!,
),
valueColor:
valuation.materialValue - holding.costAmount! >=
0
? Color(palette.up)
: Color(palette.down),
),
],
],
@@ -138,44 +212,40 @@ class HoldingDetailPage extends ConsumerWidget {
padding: EdgeInsets.zero,
child: Column(
children: [
_InfoRow(label: '金属 / 纯度', value: '${holding.metal.label} · ${holding.purityLabel}'),
_InfoRow(
label: '金属 / 纯度',
value:
'${holding.metal.label} · ${holding.purityLabel}',
),
const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '克重', value: '${holding.weightGram.toStringAsFixed(1)} g'),
_InfoRow(
label: '克重',
value: '${holding.weightGram.toStringAsFixed(1)} g',
),
const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '购买日期', value: _dateLabel(holding.purchaseDate)),
_InfoRow(
label: '购买日期',
value: _dateLabel(holding.purchaseDate),
),
const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '购买渠道', value: holding.channel ?? '未填'),
],
),
),
const SizedBox(height: 12),
PrototypeCard(
child: GestureDetector(
onTap: () {},
child: SizedBox(
height: 156,
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: 14),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _showAction(context, '编辑'),
onPressed: () =>
_showEditSheet(context, ref, holding),
child: const Text('编辑'),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton(
onPressed: () => _showAction(context, '标记卖出 / 转赠'),
onPressed: () => _markSold(context, ref, holding.id),
child: const Text('标记卖出 / 转赠'),
),
),
@@ -190,17 +260,44 @@ class HoldingDetailPage extends ConsumerWidget {
);
}
void _showAction(BuildContext context, String text) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$text 占位')));
void _showEditSheet(
BuildContext context,
WidgetRef ref,
GoldAssetHolding holding,
) {
showModalBottomSheet<void>(
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,
});
const _InfoRow({required this.label, required this.value, this.valueColor});
final String label;
final String value;
@@ -214,10 +311,17 @@ class _InfoRow extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 13, color: cs.mutedForeground)),
Text(
label,
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
),
Text(
value,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: valueColor ?? cs.foreground),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: valueColor ?? cs.foreground,
),
),
],
),
@@ -226,17 +330,28 @@ class _InfoRow extends StatelessWidget {
}
class _ChangeChip extends StatelessWidget {
const _ChangeChip({required this.amount, required this.positive});
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 ? const Color(0xFFFBEDEA) : const Color(0xFFE7F2EE),
color: positive ? upBackground : downBackground,
borderRadius: BorderRadius.circular(999),
),
child: Text(
@@ -244,13 +359,269 @@ class _ChangeChip extends StatelessWidget {
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: positive ? const Color(0xFFC0392B) : const Color(0xFF2E8B6F),
color: positive ? upColor : downColor,
),
),
);
}
}
class _EditHoldingSheet extends StatefulWidget {
const _EditHoldingSheet({required this.holding, required this.onSave});
final GoldAssetHolding holding;
final ValueChanged<GoldAssetHolding> 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')}';