From 2d7523f9019cb091195938caf4ddc13107c22c1c Mon Sep 17 00:00:00 2001 From: jingyun <> Date: Thu, 18 Jun 2026 18:08:29 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9AUI=E5=AF=B9=E9=BD=90demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/router/tab_shell.dart | 18 +- .../asset_portfolio_controller.dart | 50 ++ .../assets/presentation/assets_page.dart | 274 +++++++--- .../presentation/holding_detail_page.dart | 465 +++++++++++++++-- .../presentation/delete_account_page.dart | 24 +- .../auth/presentation/login_page.dart | 2 +- .../presentation/market_kline_page.dart | 65 ++- .../market/presentation/market_page.dart | 444 +++++++++++++--- .../news/presentation/news_detail_page.dart | 20 +- lib/features/news/presentation/news_page.dart | 41 +- .../application/price_color_theme.dart | 36 ++ .../presentation/profile_action_pages.dart | 480 +++++++++++++----- .../profile/presentation/profile_page.dart | 45 +- .../presentation/privacy_consent_page.dart | 4 +- 14 files changed, 1613 insertions(+), 355 deletions(-) create mode 100644 lib/features/profile/application/price_color_theme.dart diff --git a/lib/common/router/tab_shell.dart b/lib/common/router/tab_shell.dart index ac8b733..bd463a1 100644 --- a/lib/common/router/tab_shell.dart +++ b/lib/common/router/tab_shell.dart @@ -101,7 +101,7 @@ class _TabShellState extends State { ), bottomNavigationBar: DecoratedBox( decoration: BoxDecoration( - color: cs.background, + color: Colors.white, border: Border(top: BorderSide(color: cs.border, width: 1)), ), child: SafeArea( @@ -155,12 +155,26 @@ class _TabButton extends StatelessWidget { @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; - final color = selected ? cs.foreground : cs.mutedForeground; + final color = selected ? cs.primary : cs.mutedForeground; return InkWell( onTap: onTap, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + if (selected) + Container( + width: 22, + height: 2.5, + margin: const EdgeInsets.only(bottom: 6), + decoration: BoxDecoration( + color: cs.primary, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(3), + ), + ), + ) + else + const SizedBox(height: 8.5), Icon(icon, size: 22, color: color), const SizedBox(height: 4), Text( diff --git a/lib/features/assets/application/asset_portfolio_controller.dart b/lib/features/assets/application/asset_portfolio_controller.dart index faaefb3..9a27cb1 100644 --- a/lib/features/assets/application/asset_portfolio_controller.dart +++ b/lib/features/assets/application/asset_portfolio_controller.dart @@ -49,6 +49,56 @@ class AssetPortfolioController extends StateNotifier { ); } + void updateHolding(GoldAssetHolding holding) { + final index = _holdings.indexWhere((item) => item.id == holding.id); + if (index == -1) return; + _holdings[index] = holding; + state = _buildSummary( + holdings: _holdings, + spotPriceFor: _spotPriceFor, + changeFor: _changeFor, + recycleDiscount: recycleDiscount, + ); + } + + void removeHolding(String id) { + _holdings.removeWhere((item) => item.id == id); + state = _buildSummary( + holdings: _holdings, + spotPriceFor: _spotPriceFor, + changeFor: _changeFor, + recycleDiscount: recycleDiscount, + ); + } + + void markHoldingSold(String id) { + final index = _holdings.indexWhere((item) => item.id == id); + if (index == -1) return; + final holding = _holdings[index]; + _holdings[index] = GoldAssetHolding( + id: holding.id, + name: holding.name, + metal: holding.metal, + category: holding.category, + purity: holding.purity, + purityLabel: holding.purityLabel, + weightGram: holding.weightGram, + costAmount: holding.costAmount, + purchaseDate: holding.purchaseDate, + channel: holding.channel, + note: holding.note, + createdAt: holding.createdAt, + updatedAt: DateTime.now(), + status: HoldingStatus.sold, + ); + state = _buildSummary( + holdings: _holdings, + spotPriceFor: _spotPriceFor, + changeFor: _changeFor, + recycleDiscount: recycleDiscount, + ); + } + static PortfolioSummary _buildSummary({ required List holdings, required double Function(MetalType metal) spotPriceFor, diff --git a/lib/features/assets/presentation/assets_page.dart b/lib/features/assets/presentation/assets_page.dart index 2c9ece8..f4c42d6 100644 --- a/lib/features/assets/presentation/assets_page.dart +++ b/lib/features/assets/presentation/assets_page.dart @@ -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 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), ], diff --git a/lib/features/assets/presentation/holding_detail_page.dart b/lib/features/assets/presentation/holding_detail_page.dart index afbc300..22c6ad6 100644 --- a/lib/features/assets/presentation/holding_detail_page.dart +++ b/lib/features/assets/presentation/holding_detail_page.dart @@ -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( + 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 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')}'; diff --git a/lib/features/auth/presentation/delete_account_page.dart b/lib/features/auth/presentation/delete_account_page.dart index 7267826..4094a7d 100644 --- a/lib/features/auth/presentation/delete_account_page.dart +++ b/lib/features/auth/presentation/delete_account_page.dart @@ -669,7 +669,9 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> { final cs = ShadTheme.of(context).colorScheme; return Padding( // 键盘弹出时 sheet 跟随上移 - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), child: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), @@ -715,11 +717,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> { return [ Text( '正在清除账户数据,请稍候', - style: TextStyle( - fontSize: 13, - height: 1.55, - color: cs.mutedForeground, - ), + style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground), ), const SizedBox(height: 28), Center( @@ -737,11 +735,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> { return [ Text( '为保障账户安全,注销前需验证您的手机号。验证码将发送至 ${_maskPhone(widget.phone)}。', - style: TextStyle( - fontSize: 13, - height: 1.55, - color: cs.mutedForeground, - ), + style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground), ), if (_error != null) ...[ const SizedBox(height: 10), @@ -768,11 +762,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> { return [ Text( '验证码已发送至 ${_maskPhone(widget.phone)}', - style: TextStyle( - fontSize: 13, - height: 1.55, - color: cs.mutedForeground, - ), + style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground), ), const SizedBox(height: 20), _PinInput( @@ -864,7 +854,7 @@ class DeleteAccountSuccessPage extends ConsumerWidget { ), const SizedBox(height: 10), Text( - '您的账号及相关数据已清除完毕\n感谢您曾使用研听', + '您的账号及相关数据已清除完毕\n感谢您曾使用金值', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, diff --git a/lib/features/auth/presentation/login_page.dart b/lib/features/auth/presentation/login_page.dart index 4243850..4a0a86b 100644 --- a/lib/features/auth/presentation/login_page.dart +++ b/lib/features/auth/presentation/login_page.dart @@ -259,7 +259,7 @@ class _PhoneStep extends StatelessWidget { children: [ _SheetTitle('手机号登录', cs), const SizedBox(height: 8), - _SheetSub('未注册的手机号验证后将自动创建研听账号。', cs), + _SheetSub('未注册的手机号验证后将自动创建金值账号。', cs), const SizedBox(height: 18), Row( children: [ diff --git a/lib/features/market/presentation/market_kline_page.dart b/lib/features/market/presentation/market_kline_page.dart index d73ed6d..acb93b2 100644 --- a/lib/features/market/presentation/market_kline_page.dart +++ b/lib/features/market/presentation/market_kline_page.dart @@ -4,6 +4,8 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../common/domain/metal_type.dart'; import '../../../common/widgets/prototype_ui.dart'; +import '../../profile/application/price_color_theme.dart'; +import '../../profile/application/profile_settings_controller.dart'; import '../data/mock_market_repository.dart'; import '../data/market_models.dart'; @@ -21,6 +23,9 @@ class MarketKlinePage extends ConsumerWidget { final quote = repo.quoteFor(metal); final points = repo.chartFor(metal, period); final cs = ShadTheme.of(context).colorScheme; + final palette = priceColorPalette( + ref.watch(profileSettingsControllerProvider).priceColorMode, + ); return Scaffold( backgroundColor: const Color(0xFFFBFAF7), @@ -74,7 +79,13 @@ class MarketKlinePage extends ConsumerWidget { const SizedBox(width: 4), const Padding( padding: EdgeInsets.only(top: 8), - child: Text('元/克', style: TextStyle(fontSize: 12, color: Color(0xFF8A8780))), + child: Text( + '元/克', + style: TextStyle( + fontSize: 12, + color: Color(0xFF8A8780), + ), + ), ), ], ), @@ -83,8 +94,8 @@ class MarketKlinePage extends ConsumerWidget { '${quote.changeAmount >= 0 ? '+' : ''}${quote.changeAmount.toStringAsFixed(2)} · ${quote.changePercent.toStringAsFixed(2)}%', style: TextStyle( color: quote.changeAmount >= 0 - ? const Color(0xFFC0392B) - : const Color(0xFF2E8B6F), + ? Color(palette.up) + : Color(palette.down), fontSize: 13, fontWeight: FontWeight.w600, ), @@ -98,14 +109,15 @@ class MarketKlinePage extends ConsumerWidget { PrototypePill( label: p.label, selected: p == period, - onTap: () => Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => MarketKlinePage( - metalId: metal.id, - periodId: p.id, + onTap: () => + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => MarketKlinePage( + metalId: metal.id, + periodId: p.id, + ), + ), ), - ), - ), ), ], ), @@ -114,9 +126,19 @@ class MarketKlinePage extends ConsumerWidget { children: [ _Ohlc('开', points.first.open), const SizedBox(width: 18), - _Ohlc('高', points.map((e) => e.high).reduce((a, b) => a > b ? a : b)), + _Ohlc( + '高', + points + .map((e) => e.high) + .reduce((a, b) => a > b ? a : b), + ), const SizedBox(width: 18), - _Ohlc('低', points.map((e) => e.low).reduce((a, b) => a < b ? a : b)), + _Ohlc( + '低', + points + .map((e) => e.low) + .reduce((a, b) => a < b ? a : b), + ), const SizedBox(width: 18), _Ohlc('收', points.last.close), ], @@ -127,7 +149,9 @@ class MarketKlinePage extends ConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(10), child: PrototypeLineChart( - values: points.map((p) => p.close).toList(growable: false), + values: points + .map((p) => p.close) + .toList(growable: false), ), ), ), @@ -135,7 +159,10 @@ class MarketKlinePage extends ConsumerWidget { Text( '${quote.basisLabel} · ${_timeLabel(quote.updatedAt)}', textAlign: TextAlign.center, - style: TextStyle(fontSize: 11, color: cs.mutedForeground), + style: TextStyle( + fontSize: 11, + color: cs.mutedForeground, + ), ), ], ), @@ -161,9 +188,15 @@ class _Ohlc extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780))), + Text( + label, + style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)), + ), const SizedBox(height: 2), - Text(value.toStringAsFixed(2), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), + Text( + value.toStringAsFixed(2), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), ], ); } diff --git a/lib/features/market/presentation/market_page.dart b/lib/features/market/presentation/market_page.dart index 6ddfd48..6b8bf81 100644 --- a/lib/features/market/presentation/market_page.dart +++ b/lib/features/market/presentation/market_page.dart @@ -11,6 +11,8 @@ import '../../../common/widgets/prototype_ui.dart'; import '../application/exchange_calculator_controller.dart'; import '../application/market_controller.dart'; import '../data/market_models.dart'; +import '../../profile/application/price_color_theme.dart'; +import '../../profile/application/profile_settings_controller.dart'; class MarketPage extends HookConsumerWidget { const MarketPage({super.key}); @@ -19,23 +21,38 @@ class MarketPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final market = ref.watch(marketControllerProvider); final exchange = ref.watch(exchangeCalculatorControllerProvider); - final gramCtrl = useTextEditingController(text: exchange.gram.toStringAsFixed(2)); - final amountCtrl = useTextEditingController(text: exchange.amount.toStringAsFixed(2)); + final palette = priceColorPalette( + ref.watch(profileSettingsControllerProvider).priceColorMode, + ); + final gramCtrl = useTextEditingController( + text: exchange.gram.toStringAsFixed(2), + ); + final amountCtrl = useTextEditingController( + text: exchange.amount.toStringAsFixed(2), + ); final gramFocus = useFocusNode(); final amountFocus = useFocusNode(); final cs = ShadTheme.of(context).colorScheme; - useEffect(() { - if (!gramFocus.hasFocus) { - gramCtrl.text = exchange.gram.toStringAsFixed( - exchange.gram % 1 == 0 ? 0 : 2, - ); - } - if (!amountFocus.hasFocus) { - amountCtrl.text = exchange.amount.toStringAsFixed(2); - } - return null; - }, [exchange.gram, exchange.amount, exchange.selectedChannel.id, exchange.metal.id]); + useEffect( + () { + if (!gramFocus.hasFocus) { + gramCtrl.text = exchange.gram.toStringAsFixed( + exchange.gram % 1 == 0 ? 0 : 2, + ); + } + if (!amountFocus.hasFocus) { + amountCtrl.text = exchange.amount.toStringAsFixed(2); + } + return null; + }, + [ + exchange.gram, + exchange.amount, + exchange.selectedChannel.id, + exchange.metal.id, + ], + ); return SafeArea( top: false, @@ -50,20 +67,27 @@ class MarketPage extends HookConsumerWidget { children: [ const Text( '行情', - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + ), ), const Spacer(), - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: const Color(0xFFF7F4EE), - borderRadius: BorderRadius.circular(999), - ), - child: const Icon( - Icons.notifications_none_outlined, - size: 18, - color: Color(0xFF9A968D), + GestureDetector( + onTap: () => + _showPriceAlertSheet(context, market.selectedMetal), + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: const Color(0xFFF7F4EE), + borderRadius: BorderRadius.circular(999), + ), + child: const Icon( + Icons.notifications_none_outlined, + size: 18, + color: Color(0xFF9A968D), + ), ), ), ], @@ -75,15 +99,22 @@ class MarketPage extends HookConsumerWidget { scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: MetalType.values.length, - separatorBuilder: (context, index) => const SizedBox(width: 8), + separatorBuilder: (context, index) => + const SizedBox(width: 8), itemBuilder: (context, index) { final metal = MetalType.values[index]; return PrototypePill( label: metal.label, selected: metal == market.selectedMetal, onTap: () { - ref.read(marketControllerProvider.notifier).selectMetal(metal); - ref.read(exchangeCalculatorControllerProvider.notifier).selectMetal(metal); + ref + .read(marketControllerProvider.notifier) + .selectMetal(metal); + ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .selectMetal(metal); }, ); }, @@ -108,7 +139,9 @@ class MarketPage extends HookConsumerWidget { ), const SizedBox(width: 2), Text( - formatCny(market.quote.spotPrice).replaceFirst('¥', ''), + formatCny( + market.quote.spotPrice, + ).replaceFirst('¥', ''), style: TextStyle( fontSize: 29, fontWeight: FontWeight.w600, @@ -121,7 +154,10 @@ class MarketPage extends HookConsumerWidget { padding: EdgeInsets.only(left: 4, bottom: 2), child: Text( '元/克', - style: TextStyle(fontSize: 12, color: Color(0xFF8A8780)), + style: TextStyle( + fontSize: 12, + color: Color(0xFF8A8780), + ), ), ), ], @@ -129,16 +165,30 @@ class MarketPage extends HookConsumerWidget { const SizedBox(height: 6), Row( children: [ - _ChangeChip(amount: market.quote.changeAmount, percent: market.quote.changePercent), + _ChangeChip( + amount: market.quote.changeAmount, + percent: market.quote.changePercent, + upColor: Color(palette.up), + downColor: Color(palette.down), + upBackground: Color(palette.upBackground), + downBackground: Color(palette.downBackground), + ), const SizedBox(width: 8), Text( '报价 ${_timeLabel(market.quote.updatedAt)}', - style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF8A8780), + ), ), const Spacer(), GestureDetector( onTap: () {}, - child: const Icon(Icons.info_outline, size: 16, color: Color(0xFFB3AFA5)), + child: const Icon( + Icons.info_outline, + size: 16, + color: Color(0xFFB3AFA5), + ), ), ], ), @@ -149,9 +199,12 @@ class MarketPage extends HookConsumerWidget { _PeriodPill( label: period.label, selected: period == market.period, - onTap: () => ref.read(marketControllerProvider.notifier).selectPeriod(period), + onTap: () => ref + .read(marketControllerProvider.notifier) + .selectPeriod(period), ), - if (period != MarketPeriod.values.last) const SizedBox(width: 6), + if (period != MarketPeriod.values.last) + const SizedBox(width: 6), ], ], ), @@ -165,7 +218,9 @@ class MarketPage extends HookConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(10), child: PrototypeLineChart( - values: market.points.map((p) => p.close).toList(growable: false), + values: market.points + .map((p) => p.close) + .toList(growable: false), ), ), ), @@ -177,7 +232,10 @@ class MarketPage extends HookConsumerWidget { children: [ const Text( '兑换试算', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), const Spacer(), const PrototypePill(label: '多渠道参考', selected: true), @@ -190,14 +248,19 @@ class MarketPage extends HookConsumerWidget { scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), itemCount: exchange.channels.length, - separatorBuilder: (context, index) => const SizedBox(width: 7), + separatorBuilder: (context, index) => + const SizedBox(width: 7), itemBuilder: (context, index) { final channel = exchange.channels[index]; return PrototypePill( label: channel.name, - selected: channel.id == exchange.selectedChannel.id, + selected: + channel.id == exchange.selectedChannel.id, onTap: () => ref - .read(exchangeCalculatorControllerProvider.notifier) + .read( + exchangeCalculatorControllerProvider + .notifier, + ) .selectChannel(channel.id), ); }, @@ -212,19 +275,30 @@ class MarketPage extends HookConsumerWidget { leading: '克', onChanged: (value) { final gram = double.tryParse(value) ?? 0; - ref.read(exchangeCalculatorControllerProvider.notifier).setGram(gram); + ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .setGram(gram); }, ), const SizedBox(height: 8), Center( child: GestureDetector( - onTap: () => ref.read(exchangeCalculatorControllerProvider.notifier).toggleSwapped(), + onTap: () => ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .toggleSwapped(), child: Container( width: 30, height: 30, decoration: BoxDecoration( color: Colors.white, - border: Border.all(color: cs.border, width: 0.5), + border: Border.all( + color: cs.border, + width: 0.5, + ), shape: BoxShape.circle, boxShadow: const [ BoxShadow( @@ -234,7 +308,11 @@ class MarketPage extends HookConsumerWidget { ), ], ), - child: const Icon(Icons.swap_vert, size: 16, color: Color(0xFFB5862A)), + child: const Icon( + Icons.swap_vert, + size: 16, + color: Color(0xFFB5862A), + ), ), ), ), @@ -246,7 +324,11 @@ class MarketPage extends HookConsumerWidget { leading: '¥', onChanged: (value) { final amount = double.tryParse(value) ?? 0; - ref.read(exchangeCalculatorControllerProvider.notifier).setAmount(amount); + ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .setAmount(amount); }, ), ] else ...[ @@ -257,19 +339,30 @@ class MarketPage extends HookConsumerWidget { leading: '¥', onChanged: (value) { final amount = double.tryParse(value) ?? 0; - ref.read(exchangeCalculatorControllerProvider.notifier).setAmount(amount); + ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .setAmount(amount); }, ), const SizedBox(height: 8), Center( child: GestureDetector( - onTap: () => ref.read(exchangeCalculatorControllerProvider.notifier).toggleSwapped(), + onTap: () => ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .toggleSwapped(), child: Container( width: 30, height: 30, decoration: BoxDecoration( color: Colors.white, - border: Border.all(color: cs.border, width: 0.5), + border: Border.all( + color: cs.border, + width: 0.5, + ), shape: BoxShape.circle, boxShadow: const [ BoxShadow( @@ -279,7 +372,11 @@ class MarketPage extends HookConsumerWidget { ), ], ), - child: const Icon(Icons.swap_vert, size: 16, color: Color(0xFFB5862A)), + child: const Icon( + Icons.swap_vert, + size: 16, + color: Color(0xFFB5862A), + ), ), ), ), @@ -291,19 +388,30 @@ class MarketPage extends HookConsumerWidget { leading: '克', onChanged: (value) { final gram = double.tryParse(value) ?? 0; - ref.read(exchangeCalculatorControllerProvider.notifier).setGram(gram); + ref + .read( + exchangeCalculatorControllerProvider.notifier, + ) + .setGram(gram); }, ), ], const SizedBox(height: 10), Text( - '1 克 ${exchange.metal.label} = ${formatCny(exchange.selectedChannel.pricePerGram)} · ${exchange.selectedChannel.source} · 参考', - style: const TextStyle(fontSize: 12, color: Color(0xFF6E6A60)), + '1 克 ${exchange.metal.label} = ${formatCny(exchange.selectedChannel.pricePerGram)} · ${exchange.selectedChannel.source} · ${_timeLabel(exchange.selectedChannel.updatedAt)} · 参考', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF6E6A60), + ), ), const SizedBox(height: 5), Text( exchange.selectedChannel.hint, - style: const TextStyle(fontSize: 11, color: Color(0xFFB3AFA5), height: 1.5), + style: const TextStyle( + fontSize: 11, + color: Color(0xFFB3AFA5), + height: 1.5, + ), ), ], ), @@ -343,13 +451,18 @@ class _ExchangeField extends StatelessWidget { ), child: Row( children: [ - Text(leading, style: const TextStyle(fontSize: 14, color: Color(0xFF8A8780))), + Text( + leading, + style: const TextStyle(fontSize: 14, color: Color(0xFF8A8780)), + ), const SizedBox(width: 8), Expanded( child: TextField( controller: controller, focusNode: focusNode, - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), textAlign: TextAlign.right, decoration: const InputDecoration( border: InputBorder.none, @@ -372,7 +485,11 @@ class _ExchangeField extends StatelessWidget { } class _PeriodPill extends StatelessWidget { - const _PeriodPill({required this.label, required this.selected, required this.onTap}); + const _PeriodPill({ + required this.label, + required this.selected, + required this.onTap, + }); final String label; final bool selected; @@ -385,10 +502,21 @@ class _PeriodPill 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) { @@ -396,7 +524,7 @@ class _ChangeChip extends StatelessWidget { 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( @@ -404,13 +532,211 @@ class _ChangeChip extends StatelessWidget { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: positive ? const Color(0xFFC0392B) : const Color(0xFF2E8B6F), + color: positive ? upColor : downColor, ), ), ); } } +Future _showPriceAlertSheet(BuildContext context, MetalType metal) async { + var dir = 'up'; + final controller = TextEditingController(text: '960.00'); + try { + await showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(22)), + ), + builder: (ctx) { + return StatefulBuilder( + builder: (ctx, setState) { + return SafeArea( + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(ctx).bottom, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 14), + decoration: BoxDecoration( + color: const Color(0xFFE3DFD4), + borderRadius: BorderRadius.circular(999), + ), + ), + ), + const Text( + '设置到价提醒', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + '${metal.label} 到价时提醒你', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF8A8780), + ), + ), + const SizedBox(height: 14), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => dir = 'up'), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), + decoration: BoxDecoration( + color: dir == 'up' + ? const Color(0xFFF6ECD6) + : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: dir == 'up' + ? const Color(0xFFE3D6B4) + : const Color(0xFFECEAE3), + width: 0.5, + ), + ), + child: const Text( + '涨到', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: GestureDetector( + onTap: () => setState(() => dir = 'down'), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), + decoration: BoxDecoration( + color: dir == 'down' + ? const Color(0xFFF6ECD6) + : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: dir == 'down' + ? const Color(0xFFE3D6B4) + : const Color(0xFFECEAE3), + width: 0.5, + ), + ), + child: const Text( + '跌到', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 13, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(13), + border: Border.all( + color: const Color(0xFFE3D6B4), + width: 0.5, + ), + ), + child: Row( + children: [ + const Text( + '¥', + style: TextStyle( + fontSize: 14, + color: Color(0xFF8A8780), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + ), + textAlign: TextAlign.right, + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + ), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8), + const Text( + '元/克', + style: TextStyle( + fontSize: 13, + color: Color(0xFF8A8780), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + FilledButton( + onPressed: () { + Navigator.of(ctx).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '已设到价提醒 · $metal ${dir == 'up' ? '涨到' : '跌到'} ¥${controller.text}', + ), + ), + ); + }, + child: const Text('完成'), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } finally { + controller.dispose(); + } +} + String _timeLabel(DateTime value) { return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}'; } diff --git a/lib/features/news/presentation/news_detail_page.dart b/lib/features/news/presentation/news_detail_page.dart index 0f8e111..1b5e68d 100644 --- a/lib/features/news/presentation/news_detail_page.dart +++ b/lib/features/news/presentation/news_detail_page.dart @@ -52,7 +52,10 @@ class NewsDetailPage extends ConsumerWidget { Row( children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), decoration: BoxDecoration( color: item.metal.color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(999), @@ -69,14 +72,21 @@ class NewsDetailPage extends ConsumerWidget { const SizedBox(width: 8), Text( '${item.source} · ${_timeLabel(item.publishedAt)}', - style: TextStyle(fontSize: 12, color: cs.mutedForeground), + style: TextStyle( + fontSize: 12, + color: cs.mutedForeground, + ), ), ], ), const SizedBox(height: 12), Text( item.title, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, height: 1.4), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + height: 1.4, + ), ), const SizedBox(height: 12), for (final paragraph in item.body) ...[ @@ -90,10 +100,6 @@ class NewsDetailPage extends ConsumerWidget { ), const SizedBox(height: 12), ], - Text( - '本资讯为原型示意内容,信息整理 · 非投资建议。', - style: TextStyle(fontSize: 12, color: cs.mutedForeground, height: 1.5), - ), ], ), ), diff --git a/lib/features/news/presentation/news_page.dart b/lib/features/news/presentation/news_page.dart index 6beadc9..9c55535 100644 --- a/lib/features/news/presentation/news_page.dart +++ b/lib/features/news/presentation/news_page.dart @@ -27,11 +27,15 @@ class NewsPage extends ConsumerWidget { children: [ const Text( '资讯', - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + ), ), const Spacer(), IconButton( - onPressed: () => ref.read(newsControllerProvider.notifier).refresh(), + onPressed: () => + ref.read(newsControllerProvider.notifier).refresh(), icon: const Icon(Icons.refresh, size: 19), color: const Color(0xFF9A968D), padding: EdgeInsets.zero, @@ -42,26 +46,27 @@ class NewsPage extends ConsumerWidget { ), ], ), - const SizedBox(height: 2), - Text( - '信息整理 · 非投资建议', - style: TextStyle(fontSize: 13, color: cs.mutedForeground), - ), const SizedBox(height: 10), for (final item in news.items) ...[ PrototypeCard( margin: const EdgeInsets.only(bottom: 10), child: InkWell( - onTap: () => context.push('${AppRoutes.newsDetail}?id=${item.id}'), + onTap: () => + context.push('${AppRoutes.newsDetail}?id=${item.id}'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), decoration: BoxDecoration( - color: item.metal.color.withValues(alpha: 0.12), + color: item.metal.color.withValues( + alpha: 0.12, + ), borderRadius: BorderRadius.circular(999), ), child: Text( @@ -76,16 +81,26 @@ class NewsPage extends ConsumerWidget { const SizedBox(width: 8), Text( '${item.source} · ${_timeLabel(item.publishedAt)}', - style: TextStyle(fontSize: 12, color: cs.mutedForeground), + style: TextStyle( + fontSize: 12, + color: cs.mutedForeground, + ), ), const Spacer(), - Icon(Icons.chevron_right, size: 16, color: cs.mutedForeground), + Icon( + Icons.chevron_right, + size: 16, + color: cs.mutedForeground, + ), ], ), const SizedBox(height: 10), Text( item.title, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 8), Text( diff --git a/lib/features/profile/application/price_color_theme.dart b/lib/features/profile/application/price_color_theme.dart new file mode 100644 index 0000000..2f22cea --- /dev/null +++ b/lib/features/profile/application/price_color_theme.dart @@ -0,0 +1,36 @@ +import '../data/profile_models.dart'; + +class PriceColorPalette { + const PriceColorPalette({ + required this.up, + required this.down, + required this.upBackground, + required this.downBackground, + }); + + final int up; + final int down; + final int upBackground; + final int downBackground; +} + +const _redUpGreenDown = PriceColorPalette( + up: 0xFFC0392B, + down: 0xFF2E8B6F, + upBackground: 0xFFFBEDEA, + downBackground: 0xFFE7F2EE, +); + +const _greenUpRedDown = PriceColorPalette( + up: 0xFF2E8B6F, + down: 0xFFC0392B, + upBackground: 0xFFE7F2EE, + downBackground: 0xFFFBEDEA, +); + +PriceColorPalette priceColorPalette(PriceColorMode mode) { + return switch (mode) { + PriceColorMode.redUpGreenDown => _redUpGreenDown, + PriceColorMode.greenUpRedDown => _greenUpRedDown, + }; +} diff --git a/lib/features/profile/presentation/profile_action_pages.dart b/lib/features/profile/presentation/profile_action_pages.dart index 22e623b..64454ce 100644 --- a/lib/features/profile/presentation/profile_action_pages.dart +++ b/lib/features/profile/presentation/profile_action_pages.dart @@ -272,9 +272,9 @@ class PuritySettingsPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(profileSettingsControllerProvider); final groups = [ - ('黄金', ['足金9999', '足金999']), - ('铂金', ['PT950']), - ('白银', ['999银']), + ('黄金', ['足金9999', '足金999', '22K', '18K']), + ('铂金', ['PT990', 'PT950']), + ('白银', ['999银', '925银']), ]; return _ActionScaffold( @@ -282,17 +282,25 @@ class PuritySettingsPage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const Text( + '材料价值 = 克重 × 纯度系数 × 现货价。系数可自定义,适配不同成色。', + style: TextStyle( + fontSize: 12, + height: 1.6, + color: Color(0xFF8A8780), + ), + ), + const SizedBox(height: 14), for (final group in groups) ...[ - _SectionLabel(group.$1), + _CenteredSectionLabel(group.$1), PrototypeCard( padding: EdgeInsets.zero, child: Column( children: [ for (final purity in group.$2) ...[ - _SettingLikeRow( - icon: Icons.tune_outlined, + _MetricRow( title: purity, - trailing: (settings.purityPresets[purity] ?? 1) + value: (settings.purityPresets[purity] ?? 1) .toStringAsFixed(4), onTap: () => _toast(context, '演示:自定义「$purity」纯度系数'), ), @@ -304,6 +312,15 @@ class PuritySettingsPage extends ConsumerWidget { ), if (group != groups.last) const SizedBox(height: 12), ], + const SizedBox(height: 8), + const Text( + '点任一行可自定义(演示) · 系数仅影响材料价值估算', + style: TextStyle( + fontSize: 11, + height: 1.6, + color: Color(0xFFB3AFA5), + ), + ), ], ), ); @@ -317,8 +334,10 @@ class RecycleDiscountPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(profileSettingsControllerProvider); final notifier = ref.read(profileSettingsControllerProvider.notifier); - final preview = - 483812.0 * (settings.recycleDiscounts[MetalType.gold] ?? 0.97); + final gold = settings.recycleDiscounts[MetalType.gold] ?? 0.97; + final platinum = settings.recycleDiscounts[MetalType.platinum] ?? 0.93; + final silver = settings.recycleDiscounts[MetalType.silver] ?? 0.88; + final preview = 483812.0 * gold; return _ActionScaffold( title: '回收折扣设置', @@ -327,51 +346,88 @@ class RecycleDiscountPage extends ConsumerWidget { children: [ PrototypeCard( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text( - '回收参考预览', - style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), + const SizedBox(height: 2), + Row( + children: [ + const Text( + '回收参考预览', + style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), + ), + const Spacer(), + Text( + formatCny(preview), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + height: 1, + ), + ), + ], ), const SizedBox(height: 8), - Text( - formatCny(preview), - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - height: 1, + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + decoration: BoxDecoration( + color: const Color(0xFFF8F7F3), + borderRadius: BorderRadius.circular(14), + ), + child: Column( + children: [ + _DiscountSliderRow( + metal: MetalType.gold, + value: gold, + onChanged: (value) => + notifier.setRecycleDiscount(MetalType.gold, value), + ), + const SizedBox(height: 4), + const PrototypeDivider(margin: EdgeInsets.zero), + _DiscountSliderRow( + metal: MetalType.platinum, + value: platinum, + onChanged: (value) => notifier.setRecycleDiscount( + MetalType.platinum, + value, + ), + ), + const SizedBox(height: 4), + const PrototypeDivider(margin: EdgeInsets.zero), + _DiscountSliderRow( + metal: MetalType.silver, + value: silver, + onChanged: (value) => notifier.setRecycleDiscount( + MetalType.silver, + value, + ), + ), + ], ), ), const SizedBox(height: 8), const Text( - '按当前资产总估值和黄金折扣演示,实际以持仓金属分层计算。', + '回收参考价 = 材料价值 × 折扣系数,恒标「参考」。同一系数也用于「兑换试算」的回收口径。', + style: TextStyle( + fontSize: 12, + height: 1.6, + color: Color(0xFF8A8780), + ), + ), + const SizedBox(height: 4), + const Text( + '实务中不同金属折扣不同(足金≈95%、铂金≈78%、白银更低)。', style: TextStyle( fontSize: 11, - height: 1.5, + height: 1.6, color: Color(0xFFB3AFA5), ), ), ], ), ), - const SizedBox(height: 14), - PrototypeCard( - padding: EdgeInsets.zero, - child: Column( - children: [ - for (final entry in settings.recycleDiscounts.entries) ...[ - _DiscountSliderRow( - metal: entry.key, - value: entry.value, - onChanged: (value) => - notifier.setRecycleDiscount(entry.key, value), - ), - if (entry.key != settings.recycleDiscounts.keys.last) - const PrototypeDivider(margin: EdgeInsets.zero), - ], - ], - ), - ), ], ), ); @@ -736,73 +792,234 @@ class _WidgetPreview extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(18), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), decoration: BoxDecoration( - color: const Color(0xFF1A1A17), - borderRadius: BorderRadius.circular(20), + color: const Color(0xFFE8E0D0), + borderRadius: BorderRadius.circular(24), ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + '6月15日 周日', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: Color(0xFF5C584C), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F4EE), + borderRadius: BorderRadius.circular(22), + border: Border.all(color: const Color(0xFFDAD2C2), width: 0.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Row( + children: [ + _GoldDot(), + SizedBox(width: 7), + Text( + '金值 · 我的贵金属', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + Spacer(), + Text( + '16:11', + style: TextStyle(fontSize: 11, color: Color(0xFFB3AFA5)), + ), + ], + ), + const SizedBox(height: 14), + const Text( + '¥ 483,812', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + height: 1, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 9, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color(0xFFFBEDEA), + borderRadius: BorderRadius.circular(999), + ), + child: const Text( + '+¥5,821 · +1.21% 今日', + style: TextStyle( + fontSize: 12, + color: Color(0xFFC0392B), + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 14), + const Row( + children: [ + Expanded( + child: _MiniMetal(label: '金', value: '64%'), + ), + SizedBox(width: 8), + Expanded( + child: _MiniMetal(label: '铂', value: '22%'), + ), + SizedBox(width: 8), + Expanded( + child: _MiniMetal(label: '银', value: '14%'), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFF7F4EE), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: const Color(0xFFDAD2C2), + width: 0.5, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '黄金 元/克', + style: TextStyle( + fontSize: 12, + color: Color(0xFF8A8780), + ), + ), + const SizedBox(height: 4), + const Text( + '943.05', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + height: 1, + ), + ), + const SizedBox(height: 4), + const Text( + '▲ +0.50%', + style: TextStyle( + fontSize: 12, + color: Color(0xFFC0392B), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 32, + child: PrototypeLineChart( + values: const [ + 27, + 31, + 29, + 35, + 33, + 37, + 42, + 40, + 46, + 44, + 50, + 53, + ], + ), + ), + ], + ), + ), + ), + const SizedBox(width: 14), + const Expanded( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _MiniSquare( + color: Color(0xFFB5862A), + label: '金值', + ), + ), + SizedBox(width: 10), + Expanded(child: _MiniSquare(color: Color(0xFFCBA86A))), + ], + ), + SizedBox(height: 10), + Row( + children: [ + Expanded(child: _MiniSquare(color: Color(0xFFB7BEC6))), + SizedBox(width: 10), + Expanded(child: _MiniSquare(color: Color(0xFFC7C2B5))), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ); + } +} + +class _MiniSquare extends StatelessWidget { + const _MiniSquare({required this.color, this.label}); + + final Color color; + final String? label; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, child: Container( - padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFFF7F4EE), - borderRadius: BorderRadius.circular(18), + color: color, + borderRadius: BorderRadius.circular(16), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - _GoldDot(), - SizedBox(width: 7), - Text( - '金值', - style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700), - ), - ], - ), - const SizedBox(height: 14), - const Text( - '¥ 483,812', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - height: 1, + alignment: Alignment.center, + child: label == null + ? null + : const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.paid_outlined, color: Colors.white, size: 22), + SizedBox(height: 4), + Text( + '金值', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ], ), - ), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFFFBEDEA), - borderRadius: BorderRadius.circular(999), - ), - child: const Text( - '+¥5,821 · +1.21% 今日', - style: TextStyle( - fontSize: 12, - color: Color(0xFFC0392B), - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(height: 14), - const Row( - children: [ - Expanded( - child: _MiniMetal(label: '金', value: '64%'), - ), - SizedBox(width: 8), - Expanded( - child: _MiniMetal(label: '铂', value: '22%'), - ), - SizedBox(width: 8), - Expanded( - child: _MiniMetal(label: '银', value: '14%'), - ), - ], - ), - ], - ), ), ); } @@ -984,17 +1201,15 @@ class _SettingLikeRow extends StatelessWidget { required this.icon, required this.title, required this.trailing, - this.onTap, }); final IconData icon; final String title; final String trailing; - final VoidCallback? onTap; @override Widget build(BuildContext context) { - final content = Padding( + return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row( children: [ @@ -1014,15 +1229,11 @@ class _SettingLikeRow extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - if (onTap != null) ...[ - const SizedBox(width: 4), - const Icon(Icons.chevron_right, size: 16, color: Color(0xFFB3AFA5)), - ], + const SizedBox(width: 4), + const Icon(Icons.chevron_right, size: 16, color: Color(0xFFB3AFA5)), ], ), ); - if (onTap == null) return content; - return InkWell(onTap: onTap, child: content); } } @@ -1106,18 +1317,57 @@ class _InfoParagraph extends StatelessWidget { } } -class _SectionLabel extends StatelessWidget { - const _SectionLabel(this.label); +class _CenteredSectionLabel extends StatelessWidget { + const _CenteredSectionLabel(this.label); final String label; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.fromLTRB(2, 0, 2, 7), - child: Text( - label, - style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center( + child: Text( + label, + style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)), + ), + ), + ); + } +} + +class _MetricRow extends StatelessWidget { + const _MetricRow({ + required this.title, + required this.value, + required this.onTap, + }); + + final String title; + final String value; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + child: Row( + children: [ + Expanded(child: Text(title, style: const TextStyle(fontSize: 15))), + Text( + value, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Color(0xFF1F1D18), + ), + ), + const SizedBox(width: 6), + const Icon(Icons.chevron_right, size: 16, color: Color(0xFFB3AFA5)), + ], + ), ), ); } diff --git a/lib/features/profile/presentation/profile_page.dart b/lib/features/profile/presentation/profile_page.dart index b266cd9..59b6124 100644 --- a/lib/features/profile/presentation/profile_page.dart +++ b/lib/features/profile/presentation/profile_page.dart @@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../../../common/config/app_h5_urls.dart'; import '../../../common/router/app_router.dart'; +import '../../../common/widgets/app_toast.dart'; import '../../../common/widgets/prototype_ui.dart'; import '../../auth/application/auth_controller.dart'; import '../../auth/data/auth_models.dart'; @@ -140,14 +141,7 @@ class ProfilePage extends ConsumerWidget { backgroundColor: const Color(0xFFEAF2ED), iconColor: const Color(0xFF5E8E75), ), - ], - ), - ), - const SizedBox(height: 12), - PrototypeCard( - padding: EdgeInsets.zero, - child: Column( - children: [ + const PrototypeDivider(margin: EdgeInsets.zero), _SettingRow( icon: Icons.grid_view_outlined, title: '添加桌面小组件', @@ -156,7 +150,14 @@ class ProfilePage extends ConsumerWidget { backgroundColor: const Color(0xFFF6ECD6), iconColor: const Color(0xFFB5862A), ), - const PrototypeDivider(margin: EdgeInsets.zero), + ], + ), + ), + const SizedBox(height: 12), + PrototypeCard( + padding: EdgeInsets.zero, + child: Column( + children: [ _SettingRow( icon: Icons.tune_outlined, title: '纯度系数管理', @@ -180,7 +181,17 @@ class ProfilePage extends ConsumerWidget { icon: Icons.palette_outlined, title: '涨跌颜色', trailing: settings.priceColorMode.label, - onTap: () => _showPriceColorSheet(context, ref), + onTap: () { + final nextMode = + settings.priceColorMode == + PriceColorMode.redUpGreenDown + ? PriceColorMode.greenUpRedDown + : PriceColorMode.redUpGreenDown; + ref + .read(profileSettingsControllerProvider.notifier) + .setPriceColorMode(nextMode); + _toast(context, '已切换为「${nextMode.label}」'); + }, backgroundColor: const Color(0xFFE9F3F1), iconColor: const Color(0xFF4E8B7B), ), @@ -189,7 +200,7 @@ class ProfilePage extends ConsumerWidget { icon: Icons.currency_yen_outlined, title: '显示币种', trailing: '¥ CNY', - onTap: () => _showCurrencySheet(context), + onTap: () => _toast(context, '当前仅支持 ¥ CNY'), backgroundColor: const Color(0xFFF1EFE9), iconColor: const Color(0xFF7F7A73), ), @@ -293,10 +304,6 @@ class ProfilePage extends ConsumerWidget { context.push(AppRoutes.profileWidget); } - Future _showPriceColorSheet(BuildContext context, WidgetRef ref) async { - context.push(AppRoutes.profilePriceColor); - } - Future _showPuritySheet( BuildContext context, ProfileSettings settings, @@ -311,15 +318,15 @@ class ProfilePage extends ConsumerWidget { context.push(AppRoutes.profileRecycle); } - Future _showCurrencySheet(BuildContext context) async { - context.push(AppRoutes.profileCurrency); - } - void _showAbout(BuildContext context) { context.push(AppRoutes.profileAbout); } } +void _toast(BuildContext context, String message) { + toastInfo(msg: message); +} + class _MenuRow extends StatelessWidget { const _MenuRow({ required this.title, diff --git a/lib/features/settings/presentation/privacy_consent_page.dart b/lib/features/settings/presentation/privacy_consent_page.dart index 36e6571..df64322 100644 --- a/lib/features/settings/presentation/privacy_consent_page.dart +++ b/lib/features/settings/presentation/privacy_consent_page.dart @@ -119,7 +119,7 @@ class _PrivacyConsentPageState extends State { const _LaunchMark(size: 88), const SizedBox(height: 26), Text( - '研听', + '金值', style: TextStyle( fontSize: 42, fontWeight: FontWeight.w800, @@ -308,7 +308,7 @@ class _PrivacyConsentSheetState extends State<_PrivacyConsentSheet> { children: [ _grip(cs), Text( - '欢迎使用研听', + '欢迎使用金值', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700,