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
+16 -2
View File
@@ -101,7 +101,7 @@ class _TabShellState extends State<TabShell> {
), ),
bottomNavigationBar: DecoratedBox( bottomNavigationBar: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.background, color: Colors.white,
border: Border(top: BorderSide(color: cs.border, width: 1)), border: Border(top: BorderSide(color: cs.border, width: 1)),
), ),
child: SafeArea( child: SafeArea(
@@ -155,12 +155,26 @@ class _TabButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
final color = selected ? cs.foreground : cs.mutedForeground; final color = selected ? cs.primary : cs.mutedForeground;
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), Icon(icon, size: 22, color: color),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
@@ -49,6 +49,56 @@ class AssetPortfolioController extends StateNotifier<PortfolioSummary> {
); );
} }
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({ static PortfolioSummary _buildSummary({
required List<GoldAssetHolding> holdings, required List<GoldAssetHolding> holdings,
required double Function(MetalType metal) spotPriceFor, required double Function(MetalType metal) spotPriceFor,
+217 -57
View File
@@ -8,9 +8,10 @@ import '../../../common/domain/money.dart';
import '../../../common/router/app_router.dart'; import '../../../common/router/app_router.dart';
import '../../../common/widgets/prototype_ui.dart'; import '../../../common/widgets/prototype_ui.dart';
import '../application/asset_portfolio_controller.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 '../data/asset_models.dart';
import '../../market/application/market_controller.dart'; import '../../market/application/market_controller.dart';
import '../../profile/application/profile_settings_controller.dart';
class AssetsPage extends ConsumerWidget { class AssetsPage extends ConsumerWidget {
const AssetsPage({super.key}); const AssetsPage({super.key});
@@ -19,6 +20,7 @@ class AssetsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final summary = ref.watch(assetPortfolioControllerProvider); final summary = ref.watch(assetPortfolioControllerProvider);
final settings = ref.watch(profileSettingsControllerProvider); final settings = ref.watch(profileSettingsControllerProvider);
final palette = priceColorPalette(settings.priceColorMode);
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
final hidden = settings.amountHidden; final hidden = settings.amountHidden;
@@ -62,7 +64,9 @@ class AssetsPage extends ConsumerWidget {
.read(profileSettingsControllerProvider.notifier) .read(profileSettingsControllerProvider.notifier)
.toggleAmountHidden(), .toggleAmountHidden(),
icon: Icon( icon: Icon(
hidden ? Icons.visibility_off_outlined : Icons.visibility_outlined, hidden
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
size: 20, size: 20,
), ),
color: const Color(0xFF9A968D), color: const Color(0xFF9A968D),
@@ -97,7 +101,9 @@ class AssetsPage extends ConsumerWidget {
), ),
const SizedBox(width: 3), const SizedBox(width: 3),
Text( Text(
hidden ? '••••••' : formatCny(summary.totalValue).replaceFirst('¥', ''), hidden
? '••••••'
: formatCny(summary.totalValue).replaceFirst('¥', ''),
style: const TextStyle( style: const TextStyle(
fontSize: 64, fontSize: 64,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -114,6 +120,10 @@ class AssetsPage extends ConsumerWidget {
_ChangeChip( _ChangeChip(
amount: summary.todayChangeAmount, amount: summary.todayChangeAmount,
percent: summary.todayChangePercent, percent: summary.todayChangePercent,
upColor: Color(palette.up),
downColor: Color(palette.down),
upBackground: Color(palette.upBackground),
downBackground: Color(palette.downBackground),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
@@ -129,11 +139,13 @@ class AssetsPage extends ConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemCount: summary.breakdowns.length, itemCount: summary.breakdowns.length,
separatorBuilder: (context, index) => const SizedBox(width: 6), separatorBuilder: (context, index) =>
const SizedBox(width: 6),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = summary.breakdowns[index]; final item = summary.breakdowns[index];
return PrototypePill( return PrototypePill(
label: '${item.metal.shortLabel} ${hidden ? '•••' : formatCny(item.value)}', label:
'${item.metal.shortLabel} ${hidden ? '•••' : formatCny(item.value)}',
leading: Container( leading: Container(
width: 6, width: 6,
height: 6, height: 6,
@@ -150,7 +162,10 @@ class AssetsPage extends ConsumerWidget {
Text( Text(
'更新于 16:11 · 行情聚合', '更新于 16:11 · 行情聚合',
textAlign: TextAlign.center, 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), const SizedBox(height: 12),
GestureDetector( GestureDetector(
@@ -162,7 +177,20 @@ class AssetsPage extends ConsumerWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: PrototypeLineChart( 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, crossAxisSpacing: 10,
childAspectRatio: 1.6, childAspectRatio: 1.6,
children: [ children: [
_StatCard(title: '回收参考', value: hidden ? '¥••••' : formatCny(summary.recycleReferenceValue)), _StatCard(
title: '回收参考',
value: hidden
? '¥••••'
: formatCny(summary.recycleReferenceValue),
),
_StatCard( _StatCard(
title: '总盈亏(已填成本)', title: '总盈亏(已填成本)',
value: hidden value: hidden
? '••••' ? '••••'
: _signedCny(summary.totalValue - summary.totalCost), : _signedCny(summary.totalValue - summary.totalCost),
valueColor: summary.totalValue - summary.totalCost >= 0 valueColor: summary.totalValue - summary.totalCost >= 0
? const Color(0xFFC0392B) ? Color(palette.up)
: const Color(0xFF2E8B6F), : Color(palette.down),
),
_StatCard(
title: '总成本',
value: hidden ? '¥••••' : formatCny(summary.totalCost),
), ),
_StatCard(title: '总成本', value: hidden ? '¥••••' : formatCny(summary.totalCost)),
_StatCard( _StatCard(
title: '总克重', title: '总克重',
value: '${summary.totalWeightGram.toStringAsFixed(1)} g', value: '${summary.totalWeightGram.toStringAsFixed(1)} g',
@@ -225,6 +261,8 @@ class AssetsPage extends ConsumerWidget {
_HoldingRow( _HoldingRow(
valuation: item, valuation: item,
hidden: hidden, hidden: hidden,
upColor: Color(palette.up),
downColor: Color(palette.down),
onTap: () => context.push( onTap: () => context.push(
'${AppRoutes.assetDetail}?id=${item.holding.id}', '${AppRoutes.assetDetail}?id=${item.holding.id}',
), ),
@@ -285,11 +323,15 @@ class _HoldingRow extends StatelessWidget {
const _HoldingRow({ const _HoldingRow({
required this.valuation, required this.valuation,
required this.hidden, required this.hidden,
required this.upColor,
required this.downColor,
required this.onTap, required this.onTap,
}); });
final HoldingValuation valuation; final HoldingValuation valuation;
final bool hidden; final bool hidden;
final Color upColor;
final Color downColor;
final VoidCallback onTap; final VoidCallback onTap;
@override @override
@@ -311,7 +353,11 @@ class _HoldingRow extends StatelessWidget {
color: metal.color.withValues(alpha: 0.15), color: metal.color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10), 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), const SizedBox(width: 12),
Expanded( Expanded(
@@ -320,13 +366,19 @@ class _HoldingRow extends StatelessWidget {
children: [ children: [
Text( Text(
holding.name, holding.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${holding.purityLabel} · ${holding.weightGram.toStringAsFixed(1)}g' '${holding.purityLabel} · ${holding.weightGram.toStringAsFixed(1)}g'
'${holding.note == null ? '' : ' · ${holding.note}'}', '${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: [ children: [
Text( Text(
hidden ? '¥••••' : formatCny(valuation.materialValue), hidden ? '¥••••' : formatCny(valuation.materialValue),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${changeUp ? '' : ''}${hidden ? '¥••••' : formatCny(change.abs())}', '${changeUp ? '' : ''}${hidden ? '¥••••' : formatCny(change.abs())}',
style: TextStyle( style: TextStyle(
fontSize: 12, 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 { class _StatCard extends StatelessWidget {
const _StatCard({ const _StatCard({required this.title, required this.value, this.valueColor});
required this.title,
required this.value,
this.valueColor,
});
final String title; final String title;
final String value; final String value;
@@ -387,7 +438,10 @@ class _StatCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(title, style: TextStyle(fontSize: 12, color: cs.mutedForeground)), Text(
title,
style: TextStyle(fontSize: 12, color: cs.mutedForeground),
),
Text( Text(
value, value,
style: TextStyle( style: TextStyle(
@@ -403,10 +457,21 @@ class _StatCard extends StatelessWidget {
} }
class _ChangeChip 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 amount;
final double percent; final double percent;
final Color upColor;
final Color downColor;
final Color upBackground;
final Color downBackground;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -414,7 +479,7 @@ class _ChangeChip extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: positive ? const Color(0xFFFBEDEA) : const Color(0xFFE7F2EE), color: positive ? upBackground : downBackground,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
@@ -422,7 +487,7 @@ class _ChangeChip extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, 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]!; 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() { void _cyclePurity() {
setState(() { setState(() {
@@ -530,12 +596,15 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
final market = ref.watch(marketControllerProvider); 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; final value = _gram * _factorFor(_purity) * basePrice;
return SafeArea( return SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), padding: EdgeInsets.only(
bottom: MediaQuery.viewInsetsOf(context).bottom,
),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
@@ -559,17 +628,27 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
const Spacer(), const Spacer(),
const Text( const Text(
'添加资产', '添加资产',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
), ),
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: () => Navigator.of(context).pop(), 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 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), const SizedBox(height: 10),
Wrap( Wrap(
spacing: 9, spacing: 9,
@@ -598,23 +677,39 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
padding: const EdgeInsets.fromLTRB(16, 13, 16, 13), padding: const EdgeInsets.fromLTRB(16, 13, 16, 13),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, 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), borderRadius: BorderRadius.circular(13),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text('金属 / 纯度', style: TextStyle(fontSize: 13, color: Color(0xFF8A8780))), const Text(
'金属 / 纯度',
style: TextStyle(
fontSize: 13,
color: Color(0xFF8A8780),
),
),
GestureDetector( GestureDetector(
onTap: _cyclePurity, onTap: _cyclePurity,
child: Row( child: Row(
children: [ children: [
Text( Text(
'${_metal.label} · $_purity', '${_metal.label} · $_purity',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(width: 7), 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 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), const SizedBox(height: 9),
Row( Row(
children: [ children: [
_StepButton( _StepButton(
icon: Icons.remove, icon: Icons.remove,
onTap: () => setState(() => _gram = (_gram - 1).clamp(1, 200)), onTap: () =>
setState(() => _gram = (_gram - 1).clamp(1, 200)),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, 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), borderRadius: BorderRadius.circular(13),
), ),
child: Row( child: Row(
@@ -645,12 +750,22 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
children: [ children: [
Text( Text(
_gram.toStringAsFixed(_gram % 1 == 0 ? 0 : 1), _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 SizedBox(width: 6),
const Padding( const Padding(
padding: EdgeInsets.only(bottom: 2), 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), const SizedBox(width: 12),
_StepButton( _StepButton(
icon: Icons.add, 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text('实时材料价值', style: TextStyle(fontSize: 13, color: Color(0xFF7A5B12))), const Text(
'实时材料价值',
style: TextStyle(
fontSize: 13,
color: Color(0xFF7A5B12),
),
),
Text( Text(
formatCny(value), formatCny(value),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600), style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
),
), ),
], ],
), ),
const SizedBox(height: 7), const SizedBox(height: 7),
Text( Text(
'${_metal.label}现货 ${basePrice.toStringAsFixed(2)}/克 · 非金店零售价、非回收实收价', '${_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: [ children: [
Text( Text(
'选填:买入成本 · 日期 · 渠道 · 备注 · 照片', '选填:买入成本 · 日期 · 渠道 · 备注 · 照片',
style: TextStyle(fontSize: 13, color: cs.mutedForeground), style: TextStyle(
fontSize: 13,
color: cs.mutedForeground,
),
), ),
Icon( Icon(
_expanded ? Icons.expand_less : Icons.expand_more, _expanded ? Icons.expand_less : Icons.expand_more,
@@ -731,7 +862,9 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
label: '买入成本(含工费)', label: '买入成本(含工费)',
child: TextField( child: TextField(
controller: _costCtrl, controller: _costCtrl,
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration( decoration: const InputDecoration(
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
@@ -764,7 +897,8 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
_Chip( _Chip(
label: channel, label: channel,
selected: _channelCtrl.text == 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( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
@@ -797,15 +934,24 @@ class _AddAssetSheetState extends ConsumerState<_AddAssetSheet> {
height: 60, height: 60,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(11), 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 SizedBox(width: 10),
const Expanded( const Expanded(
child: Text( 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, purityLabel: _purity,
weightGram: _gram, weightGram: _gram,
costAmount: double.tryParse(_costCtrl.text.trim()), costAmount: double.tryParse(_costCtrl.text.trim()),
purchaseDate: DateTime.tryParse(_dateCtrl.text.trim()), purchaseDate: DateTime.tryParse(
channel: _channelCtrl.text.trim().isEmpty ? null : _channelCtrl.text.trim(), _dateCtrl.text.trim(),
note: _noteCtrl.text.trim().isEmpty ? null : _noteCtrl.text.trim(), ),
channel: _channelCtrl.text.trim().isEmpty
? null
: _channelCtrl.text.trim(),
note: _noteCtrl.text.trim().isEmpty
? null
: _noteCtrl.text.trim(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
ref.read(assetPortfolioControllerProvider.notifier).addHolding(holding); ref
.read(assetPortfolioControllerProvider.notifier)
.addHolding(holding);
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已添加 · 总估值已重算')),
);
}, },
child: const Text('保存'), child: const Text('保存'),
), ),
@@ -941,7 +1098,10 @@ class _FieldRow extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ 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), const SizedBox(width: 10),
Expanded(child: child), Expanded(child: child),
], ],
@@ -1,10 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../common/domain/money.dart'; import '../../../common/domain/money.dart';
import '../../../common/router/app_router.dart';
import '../../../common/widgets/prototype_ui.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 '../application/asset_portfolio_controller.dart';
import '../data/asset_models.dart';
class HoldingDetailPage extends ConsumerWidget { class HoldingDetailPage extends ConsumerWidget {
const HoldingDetailPage({super.key, this.id}); const HoldingDetailPage({super.key, this.id});
@@ -20,9 +25,13 @@ class HoldingDetailPage extends ConsumerWidget {
(item) => id == null || item.id == id, (item) => id == null || item.id == id,
orElse: () => summary.holdings.first.holding, orElse: () => summary.holdings.first.holding,
); );
final valuation = summary.holdings final valuation = summary.holdings.firstWhere(
.firstWhere((item) => item.holding.id == holding.id); (item) => item.holding.id == holding.id,
);
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
final palette = priceColorPalette(
ref.watch(profileSettingsControllerProvider).priceColorMode,
);
final changeUp = valuation.todayChange >= 0; final changeUp = valuation.todayChange >= 0;
return Scaffold( return Scaffold(
@@ -61,15 +70,24 @@ class HoldingDetailPage extends ConsumerWidget {
width: 36, width: 36,
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
color: holding.metal.color.withValues(alpha: 0.15), color: holding.metal.color.withValues(
alpha: 0.15,
),
borderRadius: BorderRadius.circular(10), 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), const SizedBox(width: 10),
Text( Text(
'${holding.purityLabel} · ${holding.weightGram.toStringAsFixed(1)}g', '${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: [ children: [
const Text( const Text(
'¥', '¥',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
formatCny(valuation.materialValue).replaceFirst('¥', ''), formatCny(
valuation.materialValue,
).replaceFirst('¥', ''),
style: const TextStyle( style: const TextStyle(
fontSize: 42, fontSize: 42,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -99,11 +122,18 @@ class HoldingDetailPage extends ConsumerWidget {
_ChangeChip( _ChangeChip(
amount: valuation.todayChange, amount: valuation.todayChange,
positive: changeUp, positive: changeUp,
upColor: Color(palette.up),
downColor: Color(palette.down),
upBackground: Color(palette.upBackground),
downBackground: Color(palette.downBackground),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( 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), 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( PrototypeCard(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
_InfoRow(label: '材料价值(现货)', value: formatCny(valuation.materialValue)), _InfoRow(
label: '材料价值(现货)',
value: formatCny(valuation.materialValue),
),
const PrototypeDivider(margin: EdgeInsets.zero), 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) ...[ if (holding.costAmount != null) ...[
const PrototypeDivider(margin: EdgeInsets.zero), const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '买入成本', value: formatCny(holding.costAmount!)), _InfoRow(
label: '买入成本',
value: formatCny(holding.costAmount!),
),
const PrototypeDivider(margin: EdgeInsets.zero), const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow( _InfoRow(
label: '盈亏', label: '盈亏',
value: _signedCny(valuation.materialValue - holding.costAmount!), value: _signedCny(
valueColor: valuation.materialValue - holding.costAmount! >= 0 valuation.materialValue - holding.costAmount!,
? const Color(0xFFC0392B) ),
: const Color(0xFF2E8B6F), valueColor:
valuation.materialValue - holding.costAmount! >=
0
? Color(palette.up)
: Color(palette.down),
), ),
], ],
], ],
@@ -138,44 +212,40 @@ class HoldingDetailPage extends ConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
_InfoRow(label: '金属 / 纯度', value: '${holding.metal.label} · ${holding.purityLabel}'), _InfoRow(
label: '金属 / 纯度',
value:
'${holding.metal.label} · ${holding.purityLabel}',
),
const PrototypeDivider(margin: EdgeInsets.zero), 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), const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '购买日期', value: _dateLabel(holding.purchaseDate)), _InfoRow(
label: '购买日期',
value: _dateLabel(holding.purchaseDate),
),
const PrototypeDivider(margin: EdgeInsets.zero), const PrototypeDivider(margin: EdgeInsets.zero),
_InfoRow(label: '购买渠道', value: holding.channel ?? '未填'), _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), const SizedBox(height: 14),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _showAction(context, '编辑'), onPressed: () =>
_showEditSheet(context, ref, holding),
child: const Text('编辑'), child: const Text('编辑'),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _showAction(context, '标记卖出 / 转赠'), onPressed: () => _markSold(context, ref, holding.id),
child: const Text('标记卖出 / 转赠'), child: const Text('标记卖出 / 转赠'),
), ),
), ),
@@ -190,17 +260,44 @@ class HoldingDetailPage extends ConsumerWidget {
); );
} }
void _showAction(BuildContext context, String text) { void _showEditSheet(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$text 占位'))); 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 { class _InfoRow extends StatelessWidget {
const _InfoRow({ const _InfoRow({required this.label, required this.value, this.valueColor});
required this.label,
required this.value,
this.valueColor,
});
final String label; final String label;
final String value; final String value;
@@ -214,10 +311,17 @@ class _InfoRow extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(label, style: TextStyle(fontSize: 13, color: cs.mutedForeground)), Text(
label,
style: TextStyle(fontSize: 13, color: cs.mutedForeground),
),
Text( Text(
value, 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 { 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 double amount;
final bool positive; final bool positive;
final Color upColor;
final Color downColor;
final Color upBackground;
final Color downBackground;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: positive ? const Color(0xFFFBEDEA) : const Color(0xFFE7F2EE), color: positive ? upBackground : downBackground,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
@@ -244,13 +359,269 @@ class _ChangeChip extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, 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) { String _dateLabel(DateTime? value) {
if (value == null) return '未填'; if (value == null) return '未填';
return '${value.year}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}'; return '${value.year}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}';
@@ -669,7 +669,9 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
return Padding( return Padding(
// 键盘弹出时 sheet 跟随上移 // 键盘弹出时 sheet 跟随上移
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SafeArea( child: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
@@ -715,11 +717,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
return [ return [
Text( Text(
'正在清除账户数据,请稍候', '正在清除账户数据,请稍候',
style: TextStyle( style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground),
fontSize: 13,
height: 1.55,
color: cs.mutedForeground,
),
), ),
const SizedBox(height: 28), const SizedBox(height: 28),
Center( Center(
@@ -737,11 +735,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
return [ return [
Text( Text(
'为保障账户安全,注销前需验证您的手机号。验证码将发送至 ${_maskPhone(widget.phone)}', '为保障账户安全,注销前需验证您的手机号。验证码将发送至 ${_maskPhone(widget.phone)}',
style: TextStyle( style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground),
fontSize: 13,
height: 1.55,
color: cs.mutedForeground,
),
), ),
if (_error != null) ...[ if (_error != null) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -768,11 +762,7 @@ class _VerifySheetState extends ConsumerState<_VerifySheet> {
return [ return [
Text( Text(
'验证码已发送至 ${_maskPhone(widget.phone)}', '验证码已发送至 ${_maskPhone(widget.phone)}',
style: TextStyle( style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground),
fontSize: 13,
height: 1.55,
color: cs.mutedForeground,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
_PinInput( _PinInput(
@@ -864,7 +854,7 @@ class DeleteAccountSuccessPage extends ConsumerWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'您的账号及相关数据已清除完毕\n感谢您曾使用研听', '您的账号及相关数据已清除完毕\n感谢您曾使用金值',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
@@ -259,7 +259,7 @@ class _PhoneStep extends StatelessWidget {
children: [ children: [
_SheetTitle('手机号登录', cs), _SheetTitle('手机号登录', cs),
const SizedBox(height: 8), const SizedBox(height: 8),
_SheetSub('未注册的手机号验证后将自动创建研听账号。', cs), _SheetSub('未注册的手机号验证后将自动创建金值账号。', cs),
const SizedBox(height: 18), const SizedBox(height: 18),
Row( Row(
children: [ children: [
@@ -4,6 +4,8 @@ import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../common/domain/metal_type.dart'; import '../../../common/domain/metal_type.dart';
import '../../../common/widgets/prototype_ui.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/mock_market_repository.dart';
import '../data/market_models.dart'; import '../data/market_models.dart';
@@ -21,6 +23,9 @@ class MarketKlinePage extends ConsumerWidget {
final quote = repo.quoteFor(metal); final quote = repo.quoteFor(metal);
final points = repo.chartFor(metal, period); final points = repo.chartFor(metal, period);
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
final palette = priceColorPalette(
ref.watch(profileSettingsControllerProvider).priceColorMode,
);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFFBFAF7), backgroundColor: const Color(0xFFFBFAF7),
@@ -74,7 +79,13 @@ class MarketKlinePage extends ConsumerWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
const Padding( const Padding(
padding: EdgeInsets.only(top: 8), 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)}%', '${quote.changeAmount >= 0 ? '+' : ''}${quote.changeAmount.toStringAsFixed(2)} · ${quote.changePercent.toStringAsFixed(2)}%',
style: TextStyle( style: TextStyle(
color: quote.changeAmount >= 0 color: quote.changeAmount >= 0
? const Color(0xFFC0392B) ? Color(palette.up)
: const Color(0xFF2E8B6F), : Color(palette.down),
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -98,14 +109,15 @@ class MarketKlinePage extends ConsumerWidget {
PrototypePill( PrototypePill(
label: p.label, label: p.label,
selected: p == period, selected: p == period,
onTap: () => Navigator.of(context).pushReplacement( onTap: () =>
MaterialPageRoute( Navigator.of(context).pushReplacement(
builder: (_) => MarketKlinePage( MaterialPageRoute(
metalId: metal.id, builder: (_) => MarketKlinePage(
periodId: p.id, metalId: metal.id,
periodId: p.id,
),
),
), ),
),
),
), ),
], ],
), ),
@@ -114,9 +126,19 @@ class MarketKlinePage extends ConsumerWidget {
children: [ children: [
_Ohlc('', points.first.open), _Ohlc('', points.first.open),
const SizedBox(width: 18), 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), 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), const SizedBox(width: 18),
_Ohlc('', points.last.close), _Ohlc('', points.last.close),
], ],
@@ -127,7 +149,9 @@ class MarketKlinePage extends ConsumerWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: PrototypeLineChart( 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( Text(
'${quote.basisLabel} · ${_timeLabel(quote.updatedAt)}', '${quote.basisLabel} · ${_timeLabel(quote.updatedAt)}',
textAlign: TextAlign.center, 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), 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),
),
], ],
); );
} }
+385 -59
View File
@@ -11,6 +11,8 @@ import '../../../common/widgets/prototype_ui.dart';
import '../application/exchange_calculator_controller.dart'; import '../application/exchange_calculator_controller.dart';
import '../application/market_controller.dart'; import '../application/market_controller.dart';
import '../data/market_models.dart'; import '../data/market_models.dart';
import '../../profile/application/price_color_theme.dart';
import '../../profile/application/profile_settings_controller.dart';
class MarketPage extends HookConsumerWidget { class MarketPage extends HookConsumerWidget {
const MarketPage({super.key}); const MarketPage({super.key});
@@ -19,23 +21,38 @@ class MarketPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final market = ref.watch(marketControllerProvider); final market = ref.watch(marketControllerProvider);
final exchange = ref.watch(exchangeCalculatorControllerProvider); final exchange = ref.watch(exchangeCalculatorControllerProvider);
final gramCtrl = useTextEditingController(text: exchange.gram.toStringAsFixed(2)); final palette = priceColorPalette(
final amountCtrl = useTextEditingController(text: exchange.amount.toStringAsFixed(2)); ref.watch(profileSettingsControllerProvider).priceColorMode,
);
final gramCtrl = useTextEditingController(
text: exchange.gram.toStringAsFixed(2),
);
final amountCtrl = useTextEditingController(
text: exchange.amount.toStringAsFixed(2),
);
final gramFocus = useFocusNode(); final gramFocus = useFocusNode();
final amountFocus = useFocusNode(); final amountFocus = useFocusNode();
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
useEffect(() { useEffect(
if (!gramFocus.hasFocus) { () {
gramCtrl.text = exchange.gram.toStringAsFixed( if (!gramFocus.hasFocus) {
exchange.gram % 1 == 0 ? 0 : 2, gramCtrl.text = exchange.gram.toStringAsFixed(
); exchange.gram % 1 == 0 ? 0 : 2,
} );
if (!amountFocus.hasFocus) { }
amountCtrl.text = exchange.amount.toStringAsFixed(2); if (!amountFocus.hasFocus) {
} amountCtrl.text = exchange.amount.toStringAsFixed(2);
return null; }
}, [exchange.gram, exchange.amount, exchange.selectedChannel.id, exchange.metal.id]); return null;
},
[
exchange.gram,
exchange.amount,
exchange.selectedChannel.id,
exchange.metal.id,
],
);
return SafeArea( return SafeArea(
top: false, top: false,
@@ -50,20 +67,27 @@ class MarketPage extends HookConsumerWidget {
children: [ children: [
const Text( const Text(
'行情', '行情',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
),
), ),
const Spacer(), const Spacer(),
Container( GestureDetector(
width: 30, onTap: () =>
height: 30, _showPriceAlertSheet(context, market.selectedMetal),
decoration: BoxDecoration( child: Container(
color: const Color(0xFFF7F4EE), width: 30,
borderRadius: BorderRadius.circular(999), height: 30,
), decoration: BoxDecoration(
child: const Icon( color: const Color(0xFFF7F4EE),
Icons.notifications_none_outlined, borderRadius: BorderRadius.circular(999),
size: 18, ),
color: Color(0xFF9A968D), child: const Icon(
Icons.notifications_none_outlined,
size: 18,
color: Color(0xFF9A968D),
),
), ),
), ),
], ],
@@ -75,15 +99,22 @@ class MarketPage extends HookConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemCount: MetalType.values.length, itemCount: MetalType.values.length,
separatorBuilder: (context, index) => const SizedBox(width: 8), separatorBuilder: (context, index) =>
const SizedBox(width: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final metal = MetalType.values[index]; final metal = MetalType.values[index];
return PrototypePill( return PrototypePill(
label: metal.label, label: metal.label,
selected: metal == market.selectedMetal, selected: metal == market.selectedMetal,
onTap: () { onTap: () {
ref.read(marketControllerProvider.notifier).selectMetal(metal); ref
ref.read(exchangeCalculatorControllerProvider.notifier).selectMetal(metal); .read(marketControllerProvider.notifier)
.selectMetal(metal);
ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.selectMetal(metal);
}, },
); );
}, },
@@ -108,7 +139,9 @@ class MarketPage extends HookConsumerWidget {
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
formatCny(market.quote.spotPrice).replaceFirst('¥', ''), formatCny(
market.quote.spotPrice,
).replaceFirst('¥', ''),
style: TextStyle( style: TextStyle(
fontSize: 29, fontSize: 29,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -121,7 +154,10 @@ class MarketPage extends HookConsumerWidget {
padding: EdgeInsets.only(left: 4, bottom: 2), padding: EdgeInsets.only(left: 4, bottom: 2),
child: Text( 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), const SizedBox(height: 6),
Row( Row(
children: [ 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), const SizedBox(width: 8),
Text( Text(
'报价 ${_timeLabel(market.quote.updatedAt)}', '报价 ${_timeLabel(market.quote.updatedAt)}',
style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)), style: const TextStyle(
fontSize: 12,
color: Color(0xFF8A8780),
),
), ),
const Spacer(), const Spacer(),
GestureDetector( GestureDetector(
onTap: () {}, 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( _PeriodPill(
label: period.label, label: period.label,
selected: period == market.period, 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( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: PrototypeLineChart( 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: [ children: [
const Text( const Text(
'兑换试算', '兑换试算',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
), ),
const Spacer(), const Spacer(),
const PrototypePill(label: '多渠道参考', selected: true), const PrototypePill(label: '多渠道参考', selected: true),
@@ -190,14 +248,19 @@ class MarketPage extends HookConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemCount: exchange.channels.length, itemCount: exchange.channels.length,
separatorBuilder: (context, index) => const SizedBox(width: 7), separatorBuilder: (context, index) =>
const SizedBox(width: 7),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final channel = exchange.channels[index]; final channel = exchange.channels[index];
return PrototypePill( return PrototypePill(
label: channel.name, label: channel.name,
selected: channel.id == exchange.selectedChannel.id, selected:
channel.id == exchange.selectedChannel.id,
onTap: () => ref onTap: () => ref
.read(exchangeCalculatorControllerProvider.notifier) .read(
exchangeCalculatorControllerProvider
.notifier,
)
.selectChannel(channel.id), .selectChannel(channel.id),
); );
}, },
@@ -212,19 +275,30 @@ class MarketPage extends HookConsumerWidget {
leading: '', leading: '',
onChanged: (value) { onChanged: (value) {
final gram = double.tryParse(value) ?? 0; final gram = double.tryParse(value) ?? 0;
ref.read(exchangeCalculatorControllerProvider.notifier).setGram(gram); ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.setGram(gram);
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
child: GestureDetector( child: GestureDetector(
onTap: () => ref.read(exchangeCalculatorControllerProvider.notifier).toggleSwapped(), onTap: () => ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.toggleSwapped(),
child: Container( child: Container(
width: 30, width: 30,
height: 30, height: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border.all(color: cs.border, width: 0.5), border: Border.all(
color: cs.border,
width: 0.5,
),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: const [ boxShadow: const [
BoxShadow( 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: '¥', leading: '¥',
onChanged: (value) { onChanged: (value) {
final amount = double.tryParse(value) ?? 0; final amount = double.tryParse(value) ?? 0;
ref.read(exchangeCalculatorControllerProvider.notifier).setAmount(amount); ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.setAmount(amount);
}, },
), ),
] else ...[ ] else ...[
@@ -257,19 +339,30 @@ class MarketPage extends HookConsumerWidget {
leading: '¥', leading: '¥',
onChanged: (value) { onChanged: (value) {
final amount = double.tryParse(value) ?? 0; final amount = double.tryParse(value) ?? 0;
ref.read(exchangeCalculatorControllerProvider.notifier).setAmount(amount); ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.setAmount(amount);
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
child: GestureDetector( child: GestureDetector(
onTap: () => ref.read(exchangeCalculatorControllerProvider.notifier).toggleSwapped(), onTap: () => ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.toggleSwapped(),
child: Container( child: Container(
width: 30, width: 30,
height: 30, height: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border.all(color: cs.border, width: 0.5), border: Border.all(
color: cs.border,
width: 0.5,
),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: const [ boxShadow: const [
BoxShadow( 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: '', leading: '',
onChanged: (value) { onChanged: (value) {
final gram = double.tryParse(value) ?? 0; final gram = double.tryParse(value) ?? 0;
ref.read(exchangeCalculatorControllerProvider.notifier).setGram(gram); ref
.read(
exchangeCalculatorControllerProvider.notifier,
)
.setGram(gram);
}, },
), ),
], ],
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'1 克 ${exchange.metal.label} = ${formatCny(exchange.selectedChannel.pricePerGram)} · ${exchange.selectedChannel.source} · 参考', '1 克 ${exchange.metal.label} = ${formatCny(exchange.selectedChannel.pricePerGram)} · ${exchange.selectedChannel.source} · ${_timeLabel(exchange.selectedChannel.updatedAt)} · 参考',
style: const TextStyle(fontSize: 12, color: Color(0xFF6E6A60)), style: const TextStyle(
fontSize: 12,
color: Color(0xFF6E6A60),
),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
exchange.selectedChannel.hint, 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( child: Row(
children: [ 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), const SizedBox(width: 8),
Expanded( Expanded(
child: TextField( child: TextField(
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
textAlign: TextAlign.right, textAlign: TextAlign.right,
decoration: const InputDecoration( decoration: const InputDecoration(
border: InputBorder.none, border: InputBorder.none,
@@ -372,7 +485,11 @@ class _ExchangeField extends StatelessWidget {
} }
class _PeriodPill 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 String label;
final bool selected; final bool selected;
@@ -385,10 +502,21 @@ class _PeriodPill extends StatelessWidget {
} }
class _ChangeChip 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 amount;
final double percent; final double percent;
final Color upColor;
final Color downColor;
final Color upBackground;
final Color downBackground;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -396,7 +524,7 @@ class _ChangeChip extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: positive ? const Color(0xFFFBEDEA) : const Color(0xFFE7F2EE), color: positive ? upBackground : downBackground,
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
@@ -404,13 +532,211 @@ class _ChangeChip extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: positive ? const Color(0xFFC0392B) : const Color(0xFF2E8B6F), color: positive ? upColor : downColor,
), ),
), ),
); );
} }
} }
Future<void> _showPriceAlertSheet(BuildContext context, MetalType metal) async {
var dir = 'up';
final controller = TextEditingController(text: '960.00');
try {
await showModalBottomSheet<void>(
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) { String _timeLabel(DateTime value) {
return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}'; return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}';
} }
@@ -52,7 +52,10 @@ class NewsDetailPage extends ConsumerWidget {
Row( Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: item.metal.color.withValues(alpha: 0.12), color: item.metal.color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
@@ -69,14 +72,21 @@ class NewsDetailPage extends ConsumerWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'${item.source} · ${_timeLabel(item.publishedAt)}', '${item.source} · ${_timeLabel(item.publishedAt)}',
style: TextStyle(fontSize: 12, color: cs.mutedForeground), style: TextStyle(
fontSize: 12,
color: cs.mutedForeground,
),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
item.title, 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), const SizedBox(height: 12),
for (final paragraph in item.body) ...[ for (final paragraph in item.body) ...[
@@ -90,10 +100,6 @@ class NewsDetailPage extends ConsumerWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
Text(
'本资讯为原型示意内容,信息整理 · 非投资建议。',
style: TextStyle(fontSize: 12, color: cs.mutedForeground, height: 1.5),
),
], ],
), ),
), ),
+28 -13
View File
@@ -27,11 +27,15 @@ class NewsPage extends ConsumerWidget {
children: [ children: [
const Text( const Text(
'资讯', '资讯',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
),
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
onPressed: () => ref.read(newsControllerProvider.notifier).refresh(), onPressed: () =>
ref.read(newsControllerProvider.notifier).refresh(),
icon: const Icon(Icons.refresh, size: 19), icon: const Icon(Icons.refresh, size: 19),
color: const Color(0xFF9A968D), color: const Color(0xFF9A968D),
padding: EdgeInsets.zero, 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), const SizedBox(height: 10),
for (final item in news.items) ...[ for (final item in news.items) ...[
PrototypeCard( PrototypeCard(
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
child: InkWell( child: InkWell(
onTap: () => context.push('${AppRoutes.newsDetail}?id=${item.id}'), onTap: () =>
context.push('${AppRoutes.newsDetail}?id=${item.id}'),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: item.metal.color.withValues(alpha: 0.12), color: item.metal.color.withValues(
alpha: 0.12,
),
borderRadius: BorderRadius.circular(999), borderRadius: BorderRadius.circular(999),
), ),
child: Text( child: Text(
@@ -76,16 +81,26 @@ class NewsPage extends ConsumerWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'${item.source} · ${_timeLabel(item.publishedAt)}', '${item.source} · ${_timeLabel(item.publishedAt)}',
style: TextStyle(fontSize: 12, color: cs.mutedForeground), style: TextStyle(
fontSize: 12,
color: cs.mutedForeground,
),
), ),
const Spacer(), const Spacer(),
Icon(Icons.chevron_right, size: 16, color: cs.mutedForeground), Icon(
Icons.chevron_right,
size: 16,
color: cs.mutedForeground,
),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
item.title, item.title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
@@ -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,
};
}
@@ -272,9 +272,9 @@ class PuritySettingsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(profileSettingsControllerProvider); final settings = ref.watch(profileSettingsControllerProvider);
final groups = [ final groups = [
('黄金', ['足金9999', '足金999']), ('黄金', ['足金9999', '足金999', '22K', '18K']),
('铂金', ['PT950']), ('铂金', ['PT990', 'PT950']),
('白银', ['999银']), ('白银', ['999银', '925银']),
]; ];
return _ActionScaffold( return _ActionScaffold(
@@ -282,17 +282,25 @@ class PuritySettingsPage extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text(
'材料价值 = 克重 × 纯度系数 × 现货价。系数可自定义,适配不同成色。',
style: TextStyle(
fontSize: 12,
height: 1.6,
color: Color(0xFF8A8780),
),
),
const SizedBox(height: 14),
for (final group in groups) ...[ for (final group in groups) ...[
_SectionLabel(group.$1), _CenteredSectionLabel(group.$1),
PrototypeCard( PrototypeCard(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
for (final purity in group.$2) ...[ for (final purity in group.$2) ...[
_SettingLikeRow( _MetricRow(
icon: Icons.tune_outlined,
title: purity, title: purity,
trailing: (settings.purityPresets[purity] ?? 1) value: (settings.purityPresets[purity] ?? 1)
.toStringAsFixed(4), .toStringAsFixed(4),
onTap: () => _toast(context, '演示:自定义「$purity」纯度系数'), onTap: () => _toast(context, '演示:自定义「$purity」纯度系数'),
), ),
@@ -304,6 +312,15 @@ class PuritySettingsPage extends ConsumerWidget {
), ),
if (group != groups.last) const SizedBox(height: 12), 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) { Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(profileSettingsControllerProvider); final settings = ref.watch(profileSettingsControllerProvider);
final notifier = ref.read(profileSettingsControllerProvider.notifier); final notifier = ref.read(profileSettingsControllerProvider.notifier);
final preview = final gold = settings.recycleDiscounts[MetalType.gold] ?? 0.97;
483812.0 * (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( return _ActionScaffold(
title: '回收折扣设置', title: '回收折扣设置',
@@ -327,51 +346,88 @@ class RecycleDiscountPage extends ConsumerWidget {
children: [ children: [
PrototypeCard( PrototypeCard(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text( const SizedBox(height: 2),
'回收参考预览', Row(
style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), 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), const SizedBox(height: 8),
Text( Container(
formatCny(preview), padding: const EdgeInsets.symmetric(
style: const TextStyle( horizontal: 16,
fontSize: 28, vertical: 16,
fontWeight: FontWeight.w700, ),
height: 1, 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 SizedBox(height: 8),
const Text( const Text(
'按当前资产总估值和黄金折扣演示,实际以持仓金属分层计算', '回收参考价 = 材料价值 × 折扣系数,恒标「参考」。同一系数也用于「兑换试算」的回收口径',
style: TextStyle(
fontSize: 12,
height: 1.6,
color: Color(0xFF8A8780),
),
),
const SizedBox(height: 4),
const Text(
'实务中不同金属折扣不同(足金≈95%、铂金≈78%、白银更低)。',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
height: 1.5, height: 1.6,
color: Color(0xFFB3AFA5), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A17), color: const Color(0xFFE8E0D0),
borderRadius: BorderRadius.circular(20), 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( child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF7F4EE), color: color,
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(16),
), ),
child: Column( alignment: Alignment.center,
crossAxisAlignment: CrossAxisAlignment.start, child: label == null
children: [ ? null
const Row( : const Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
_GoldDot(), children: [
SizedBox(width: 7), Icon(Icons.paid_outlined, color: Colors.white, size: 22),
Text( SizedBox(height: 4),
'金值', Text(
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700), '金值',
), style: TextStyle(
], color: Colors.white,
), fontSize: 10,
const SizedBox(height: 14), fontWeight: FontWeight.w700,
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%'),
),
],
),
],
),
), ),
); );
} }
@@ -984,17 +1201,15 @@ class _SettingLikeRow extends StatelessWidget {
required this.icon, required this.icon,
required this.title, required this.title,
required this.trailing, required this.trailing,
this.onTap,
}); });
final IconData icon; final IconData icon;
final String title; final String title;
final String trailing; final String trailing;
final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final content = Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
child: Row( child: Row(
children: [ children: [
@@ -1014,15 +1229,11 @@ class _SettingLikeRow extends StatelessWidget {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
if (onTap != null) ...[ const SizedBox(width: 4),
const SizedBox(width: 4), const Icon(Icons.chevron_right, size: 16, color: Color(0xFFB3AFA5)),
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 { class _CenteredSectionLabel extends StatelessWidget {
const _SectionLabel(this.label); const _CenteredSectionLabel(this.label);
final String label; final String label;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(2, 0, 2, 7), padding: const EdgeInsets.symmetric(vertical: 12),
child: Text( child: Center(
label, child: Text(
style: const TextStyle(fontSize: 12, color: Color(0xFF8A8780)), 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)),
],
),
), ),
); );
} }
@@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../../../common/config/app_h5_urls.dart'; import '../../../common/config/app_h5_urls.dart';
import '../../../common/router/app_router.dart'; import '../../../common/router/app_router.dart';
import '../../../common/widgets/app_toast.dart';
import '../../../common/widgets/prototype_ui.dart'; import '../../../common/widgets/prototype_ui.dart';
import '../../auth/application/auth_controller.dart'; import '../../auth/application/auth_controller.dart';
import '../../auth/data/auth_models.dart'; import '../../auth/data/auth_models.dart';
@@ -140,14 +141,7 @@ class ProfilePage extends ConsumerWidget {
backgroundColor: const Color(0xFFEAF2ED), backgroundColor: const Color(0xFFEAF2ED),
iconColor: const Color(0xFF5E8E75), iconColor: const Color(0xFF5E8E75),
), ),
], const PrototypeDivider(margin: EdgeInsets.zero),
),
),
const SizedBox(height: 12),
PrototypeCard(
padding: EdgeInsets.zero,
child: Column(
children: [
_SettingRow( _SettingRow(
icon: Icons.grid_view_outlined, icon: Icons.grid_view_outlined,
title: '添加桌面小组件', title: '添加桌面小组件',
@@ -156,7 +150,14 @@ class ProfilePage extends ConsumerWidget {
backgroundColor: const Color(0xFFF6ECD6), backgroundColor: const Color(0xFFF6ECD6),
iconColor: const Color(0xFFB5862A), iconColor: const Color(0xFFB5862A),
), ),
const PrototypeDivider(margin: EdgeInsets.zero), ],
),
),
const SizedBox(height: 12),
PrototypeCard(
padding: EdgeInsets.zero,
child: Column(
children: [
_SettingRow( _SettingRow(
icon: Icons.tune_outlined, icon: Icons.tune_outlined,
title: '纯度系数管理', title: '纯度系数管理',
@@ -180,7 +181,17 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.palette_outlined, icon: Icons.palette_outlined,
title: '涨跌颜色', title: '涨跌颜色',
trailing: settings.priceColorMode.label, 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), backgroundColor: const Color(0xFFE9F3F1),
iconColor: const Color(0xFF4E8B7B), iconColor: const Color(0xFF4E8B7B),
), ),
@@ -189,7 +200,7 @@ class ProfilePage extends ConsumerWidget {
icon: Icons.currency_yen_outlined, icon: Icons.currency_yen_outlined,
title: '显示币种', title: '显示币种',
trailing: '¥ CNY', trailing: '¥ CNY',
onTap: () => _showCurrencySheet(context), onTap: () => _toast(context, '当前仅支持 ¥ CNY'),
backgroundColor: const Color(0xFFF1EFE9), backgroundColor: const Color(0xFFF1EFE9),
iconColor: const Color(0xFF7F7A73), iconColor: const Color(0xFF7F7A73),
), ),
@@ -293,10 +304,6 @@ class ProfilePage extends ConsumerWidget {
context.push(AppRoutes.profileWidget); context.push(AppRoutes.profileWidget);
} }
Future<void> _showPriceColorSheet(BuildContext context, WidgetRef ref) async {
context.push(AppRoutes.profilePriceColor);
}
Future<void> _showPuritySheet( Future<void> _showPuritySheet(
BuildContext context, BuildContext context,
ProfileSettings settings, ProfileSettings settings,
@@ -311,15 +318,15 @@ class ProfilePage extends ConsumerWidget {
context.push(AppRoutes.profileRecycle); context.push(AppRoutes.profileRecycle);
} }
Future<void> _showCurrencySheet(BuildContext context) async {
context.push(AppRoutes.profileCurrency);
}
void _showAbout(BuildContext context) { void _showAbout(BuildContext context) {
context.push(AppRoutes.profileAbout); context.push(AppRoutes.profileAbout);
} }
} }
void _toast(BuildContext context, String message) {
toastInfo(msg: message);
}
class _MenuRow extends StatelessWidget { class _MenuRow extends StatelessWidget {
const _MenuRow({ const _MenuRow({
required this.title, required this.title,
@@ -119,7 +119,7 @@ class _PrivacyConsentPageState extends State<PrivacyConsentPage> {
const _LaunchMark(size: 88), const _LaunchMark(size: 88),
const SizedBox(height: 26), const SizedBox(height: 26),
Text( Text(
'研听', '金值',
style: TextStyle( style: TextStyle(
fontSize: 42, fontSize: 42,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@@ -308,7 +308,7 @@ class _PrivacyConsentSheetState extends State<_PrivacyConsentSheet> {
children: [ children: [
_grip(cs), _grip(cs),
Text( Text(
'欢迎使用研听', '欢迎使用金值',
style: TextStyle( style: TextStyle(
fontSize: 19, fontSize: 19,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,