228 lines
8.2 KiB
Dart
228 lines
8.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
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';
|
|
|
|
class MarketKlinePage extends ConsumerWidget {
|
|
const MarketKlinePage({super.key, this.metalId, this.periodId});
|
|
|
|
final String? metalId;
|
|
final String? periodId;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final repo = ref.watch(mockMarketRepositoryProvider);
|
|
final metal = _metalFromId(metalId) ?? MetalType.gold;
|
|
final period = _periodFromId(periodId) ?? MarketPeriod.h24;
|
|
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),
|
|
appBar: AppBar(
|
|
backgroundColor: const Color(0xFFFBFAF7),
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
centerTitle: true,
|
|
leading: IconButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
icon: const Icon(Icons.chevron_left, size: 20),
|
|
),
|
|
title: Text(
|
|
'${metal.label} K线',
|
|
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: ListView(
|
|
padding: const EdgeInsets.fromLTRB(0, 8, 0, 24),
|
|
children: [
|
|
PrototypeFrame(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const SizedBox(height: 4),
|
|
PrototypeCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'¥',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: cs.foreground,
|
|
),
|
|
),
|
|
const SizedBox(width: 2),
|
|
Text(
|
|
quote.spotPrice.toStringAsFixed(2),
|
|
style: const TextStyle(
|
|
fontSize: 29,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -1,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
const Padding(
|
|
padding: EdgeInsets.only(top: 8),
|
|
child: Text(
|
|
'元/克',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Color(0xFF8A8780),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'${quote.changeAmount >= 0 ? '+' : ''}${quote.changeAmount.toStringAsFixed(2)} · ${quote.changePercent.toStringAsFixed(2)}%',
|
|
style: TextStyle(
|
|
color: quote.changeAmount >= 0
|
|
? Color(palette.up)
|
|
: Color(palette.down),
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
for (final p in MarketPeriod.values)
|
|
PrototypePill(
|
|
label: p.label,
|
|
selected: p == period,
|
|
onTap: () =>
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute(
|
|
builder: (_) => MarketKlinePage(
|
|
metalId: metal.id,
|
|
periodId: p.id,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
children: [
|
|
_Ohlc('开', points.first.open),
|
|
const SizedBox(width: 18),
|
|
_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),
|
|
),
|
|
const SizedBox(width: 18),
|
|
_Ohlc('收', points.last.close),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 300,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: PrototypeLineChart(
|
|
values: points
|
|
.map((p) => p.close)
|
|
.toList(growable: false),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'${quote.basisLabel} · ${_timeLabel(quote.updatedAt)}',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: cs.mutedForeground,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Ohlc extends StatelessWidget {
|
|
const _Ohlc(this.label, this.value);
|
|
|
|
final String label;
|
|
final double value;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
MetalType? _metalFromId(String? id) {
|
|
return switch (id) {
|
|
'gold' => MetalType.gold,
|
|
'platinum' => MetalType.platinum,
|
|
'silver' => MetalType.silver,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
MarketPeriod? _periodFromId(String? id) {
|
|
return switch (id) {
|
|
'24h' => MarketPeriod.h24,
|
|
'5d' => MarketPeriod.d5,
|
|
'1m' => MarketPeriod.m1,
|
|
'3m' => MarketPeriod.m3,
|
|
'1y' => MarketPeriod.y1,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
String _timeLabel(DateTime value) {
|
|
return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}';
|
|
}
|