本次提交包含多项迭代优化和问题修复: 1. 新增缩略图图片组件、数字格式化工具类,补充多语言翻译类型与本地化支持 2. 优化底部导航栏主题色统一使用动态accent色值 3. 修复多处图表动画、路由跳转、API请求相关问题 4. 简化服务器公告文案,调整默认分屏状态为关闭 5. 新增安卓/iOS桌面快捷方式配置 6. 重构多处状态管理类使用SafeNotifierInit统一异常保护 7. 替换硬编码蓝色为主题色,更新版本号获取方式为动态读取 8. 优化缓存预加载逻辑,移除无用代码 9. 调整默认设置项,优化用户体验细节
824 lines
25 KiB
Dart
824 lines
25 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 汇率换算页面
|
||
/// 创建时间: 2026-05-30
|
||
/// 更新时间: 2026-05-30
|
||
/// 作用: iOS风格汇率换算界面,集成exchangerate-api.com实时数据
|
||
/// 上次更新: 初始版本
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:intl/intl.dart';
|
||
|
||
import 'package:xianyan/core/theme/app_theme.dart';
|
||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||
import 'package:xianyan/core/theme/app_typography.dart';
|
||
import 'package:xianyan/core/theme/app_radius.dart';
|
||
import 'package:xianyan/features/discover/services/exchange_rate_service.dart';
|
||
import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart';
|
||
|
||
/// ── 汇率换算状态 ──
|
||
class ExchangeRateState {
|
||
const ExchangeRateState({
|
||
this.fromCurrency = 'CNY',
|
||
this.toCurrency = 'USD',
|
||
this.amount = 1.0,
|
||
this.result,
|
||
this.isLoading = false,
|
||
this.error,
|
||
this.rates,
|
||
});
|
||
|
||
final String fromCurrency;
|
||
final String toCurrency;
|
||
final double amount;
|
||
final ExchangeRateResult? result;
|
||
final bool isLoading;
|
||
final String? error;
|
||
final Map<String, double>? rates;
|
||
|
||
ExchangeRateState copyWith({
|
||
String? fromCurrency,
|
||
String? toCurrency,
|
||
double? amount,
|
||
ExchangeRateResult? result,
|
||
bool? isLoading,
|
||
String? error,
|
||
Map<String, double>? rates,
|
||
}) {
|
||
return ExchangeRateState(
|
||
fromCurrency: fromCurrency ?? this.fromCurrency,
|
||
toCurrency: toCurrency ?? this.toCurrency,
|
||
amount: amount ?? this.amount,
|
||
result: result ?? this.result,
|
||
isLoading: isLoading ?? this.isLoading,
|
||
error: error,
|
||
rates: rates ?? this.rates,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// ── 汇率换算 Notifier ──
|
||
class ExchangeRateNotifier extends Notifier<ExchangeRateState> {
|
||
@override
|
||
ExchangeRateState build() {
|
||
return const ExchangeRateState();
|
||
}
|
||
|
||
/// 换算
|
||
Future<void> convert() async {
|
||
final s = state;
|
||
state = s.copyWith(isLoading: true);
|
||
|
||
final result = await ExchangeRateService.convert(
|
||
from: s.fromCurrency,
|
||
to: s.toCurrency,
|
||
amount: s.amount,
|
||
);
|
||
|
||
if (result != null) {
|
||
state = state.copyWith(
|
||
isLoading: false,
|
||
result: result,
|
||
);
|
||
} else {
|
||
state = state.copyWith(
|
||
isLoading: false,
|
||
error: '汇率查询失败,请检查网络',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 设置源货币
|
||
void setFromCurrency(String code) {
|
||
state = state.copyWith(fromCurrency: code);
|
||
_autoConvert();
|
||
}
|
||
|
||
/// 设置目标货币
|
||
void setToCurrency(String code) {
|
||
state = state.copyWith(toCurrency: code);
|
||
_autoConvert();
|
||
}
|
||
|
||
/// 设置金额
|
||
void setAmount(double value) {
|
||
state = state.copyWith(amount: value);
|
||
}
|
||
|
||
/// 交换货币
|
||
void swapCurrencies() {
|
||
final s = state;
|
||
state = s.copyWith(
|
||
fromCurrency: s.toCurrency,
|
||
toCurrency: s.fromCurrency,
|
||
);
|
||
_autoConvert();
|
||
}
|
||
|
||
/// 自动换算(切换货币时触发)
|
||
void _autoConvert() {
|
||
convert();
|
||
}
|
||
|
||
/// 加载全部汇率(用于热门货币网格)
|
||
Future<void> loadRates() async {
|
||
final rates = await ExchangeRateService.getRates(state.fromCurrency);
|
||
if (rates != null) {
|
||
state = state.copyWith(rates: rates);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// ── Provider ──
|
||
final exchangeRateProvider =
|
||
NotifierProvider<ExchangeRateNotifier, ExchangeRateState>(
|
||
ExchangeRateNotifier.new,
|
||
);
|
||
|
||
/// ── 汇率换算页面 ──
|
||
class ExchangeRatePage extends ConsumerStatefulWidget {
|
||
const ExchangeRatePage({super.key});
|
||
|
||
@override
|
||
ConsumerState<ExchangeRatePage> createState() => _ExchangeRatePageState();
|
||
}
|
||
|
||
class _ExchangeRatePageState extends ConsumerState<ExchangeRatePage> {
|
||
final _amountController = TextEditingController(text: '1');
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
ref.read(exchangeRateProvider.notifier).convert();
|
||
ref.read(exchangeRateProvider.notifier).loadRates();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_amountController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
/// 提交换算
|
||
void _submit() {
|
||
final text = _amountController.text.trim();
|
||
final amount = double.tryParse(text);
|
||
if (amount == null || amount <= 0) return;
|
||
ref.read(exchangeRateProvider.notifier).setAmount(amount);
|
||
ref.read(exchangeRateProvider.notifier).convert();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = AppTheme.ext(context);
|
||
final state = ref.watch(exchangeRateProvider);
|
||
|
||
return CupertinoPageScaffold(
|
||
navigationBar: CupertinoNavigationBar(
|
||
middle: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Text('💱', style: TextStyle(fontSize: 18)),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'汇率换算',
|
||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||
),
|
||
],
|
||
),
|
||
backgroundColor: ext.bgPrimary.withValues(alpha: 0.9),
|
||
border: null,
|
||
leading: const AdaptiveBackButton(),
|
||
trailing: _buildNetworkBadge(ext),
|
||
),
|
||
child: SafeArea(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
_buildCurrencySelector(ext, state),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildAmountInput(ext),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildConvertButton(ext, state),
|
||
if (state.isLoading) _buildLoading(ext),
|
||
if (state.error != null) _buildError(ext, state.error!),
|
||
if (state.result != null) _buildResult(ext, state.result!),
|
||
const SizedBox(height: AppSpacing.lg),
|
||
_buildPopularGrid(ext, state),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 网络标识 ──
|
||
Widget _buildNetworkBadge(AppThemeExtension ext) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.xs,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(CupertinoIcons.globe, size: 12, color: ext.accent),
|
||
const SizedBox(width: 3),
|
||
Text(
|
||
'联网',
|
||
style: AppTypography.caption2.copyWith(color: ext.accent),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 货币选择器 ──
|
||
Widget _buildCurrencySelector(AppThemeExtension ext, ExchangeRateState state) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgCard,
|
||
borderRadius: AppRadius.lgBorder,
|
||
border: Border.all(color: ext.accent.withValues(alpha: 0.15)),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
_buildCurrencyRow(
|
||
ext,
|
||
label: '从',
|
||
currencyCode: state.fromCurrency,
|
||
onTap: () => _showCurrencyPicker(
|
||
isFrom: true,
|
||
currentCode: state.fromCurrency,
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Container(
|
||
height: 0.5,
|
||
color: ext.textHint.withValues(alpha: 0.2),
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: () =>
|
||
ref.read(exchangeRateProvider.notifier).swapCurrencies(),
|
||
child: Container(
|
||
margin: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
),
|
||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||
decoration: BoxDecoration(
|
||
color: ext.accent.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.mdBorder,
|
||
),
|
||
child: Icon(
|
||
CupertinoIcons.arrow_up_down,
|
||
size: 18,
|
||
color: ext.accent,
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Container(
|
||
height: 0.5,
|
||
color: ext.textHint.withValues(alpha: 0.2),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
_buildCurrencyRow(
|
||
ext,
|
||
label: '到',
|
||
currencyCode: state.toCurrency,
|
||
onTap: () => _showCurrencyPicker(
|
||
isFrom: false,
|
||
currentCode: state.toCurrency,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 单行货币 ──
|
||
Widget _buildCurrencyRow(
|
||
AppThemeExtension ext, {
|
||
required String label,
|
||
required String currencyCode,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
final info = ExchangeRateService.currencyMap[currencyCode];
|
||
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
info?.flag ?? '🌍',
|
||
style: const TextStyle(fontSize: 28),
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
currencyCode,
|
||
style: AppTypography.headline.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
Text(
|
||
info?.name ?? currencyCode,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Icon(
|
||
CupertinoIcons.chevron_right,
|
||
size: 16,
|
||
color: ext.textHint,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 金额输入 ──
|
||
Widget _buildAmountInput(AppThemeExtension ext) {
|
||
final state = ref.watch(exchangeRateProvider);
|
||
final info = ExchangeRateService.currencyMap[state.fromCurrency];
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'金额',
|
||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||
),
|
||
const SizedBox(height: AppSpacing.xs),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: ext.bgSecondary,
|
||
borderRadius: AppRadius.lgBorder,
|
||
),
|
||
child: CupertinoTextField(
|
||
controller: _amountController,
|
||
placeholder: '输入金额',
|
||
placeholderStyle: AppTypography.body.copyWith(
|
||
color: ext.textHint,
|
||
),
|
||
style: AppTypography.headline.copyWith(color: ext.textPrimary),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm + 4,
|
||
),
|
||
decoration: const BoxDecoration(),
|
||
keyboardType: const TextInputType.numberWithOptions(
|
||
decimal: true,
|
||
),
|
||
inputFormatters: [
|
||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
|
||
],
|
||
prefix: Padding(
|
||
padding: const EdgeInsets.only(left: AppSpacing.md),
|
||
child: Text(
|
||
info?.symbol ?? '',
|
||
style: AppTypography.headline.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
),
|
||
onSubmitted: (_) => _submit(),
|
||
onChanged: (value) {
|
||
final amount = double.tryParse(value.trim());
|
||
if (amount != null) {
|
||
ref.read(exchangeRateProvider.notifier).setAmount(amount);
|
||
}
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// ── 换算按钮 ──
|
||
Widget _buildConvertButton(AppThemeExtension ext, ExchangeRateState state) {
|
||
return SizedBox(
|
||
width: double.infinity,
|
||
child: CupertinoButton(
|
||
color: ext.accent,
|
||
borderRadius: AppRadius.lgBorder,
|
||
onPressed: state.isLoading ? null : _submit,
|
||
child: state.isLoading
|
||
? const CupertinoActivityIndicator(color: CupertinoColors.white)
|
||
: Text(
|
||
'💱 换算',
|
||
style: AppTypography.title3.copyWith(
|
||
color: CupertinoColors.white,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 加载指示器 ──
|
||
Widget _buildLoading(AppThemeExtension ext) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: AppSpacing.lg),
|
||
child: Center(
|
||
child: Column(
|
||
children: [
|
||
const CupertinoActivityIndicator(),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
Text(
|
||
'正在查询汇率...',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 错误提示 ──
|
||
Widget _buildError(AppThemeExtension ext, String error) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: AppSpacing.lg),
|
||
child: Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
color: CupertinoColors.systemRed.withValues(alpha: 0.1),
|
||
borderRadius: AppRadius.lgBorder,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(
|
||
CupertinoIcons.xmark_circle,
|
||
color: CupertinoColors.systemRed,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Text(
|
||
error,
|
||
style: AppTypography.body.copyWith(
|
||
color: CupertinoColors.systemRed,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 换算结果 ──
|
||
Widget _buildResult(AppThemeExtension ext, ExchangeRateResult result) {
|
||
final toInfo = ExchangeRateService.currencyMap[result.to];
|
||
final fromInfo = ExchangeRateService.currencyMap[result.from];
|
||
final fmt = NumberFormat('#,##0.00');
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: AppSpacing.lg),
|
||
child: Container(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
decoration: BoxDecoration(
|
||
color: ext.bgCard,
|
||
borderRadius: AppRadius.lgBorder,
|
||
border: Border.all(color: ext.accent.withValues(alpha: 0.2)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
CupertinoIcons.checkmark_circle,
|
||
color: ext.accent,
|
||
size: 20,
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
'换算结果',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.accent,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: AppSpacing.md),
|
||
_buildResultRow(
|
||
ext,
|
||
label: '输入金额',
|
||
value: '${fromInfo?.symbol ?? ''}${fmt.format(result.amount)} ${result.from}',
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_buildResultRow(
|
||
ext,
|
||
label: '换算结果',
|
||
value: '${toInfo?.symbol ?? ''}${fmt.format(result.converted)} ${result.to}',
|
||
isHighlight: true,
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_buildResultRow(
|
||
ext,
|
||
label: '参考汇率',
|
||
value: '1 ${result.from} = ${fmt.format(result.rate)} ${result.to}',
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
_buildResultRow(
|
||
ext,
|
||
label: '更新时间',
|
||
value: _formatDate(result.updateTime),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 结果行 ──
|
||
Widget _buildResultRow(
|
||
AppThemeExtension ext, {
|
||
required String label,
|
||
required String value,
|
||
bool isHighlight = false,
|
||
}) {
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
width: 72,
|
||
child: Text(
|
||
label,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Text(
|
||
value,
|
||
style: (isHighlight ? AppTypography.headline : AppTypography.body)
|
||
.copyWith(
|
||
color: isHighlight ? ext.accent : ext.textPrimary,
|
||
fontWeight: isHighlight ? FontWeight.w600 : FontWeight.w400,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// ── 热门货币网格 ──
|
||
Widget _buildPopularGrid(AppThemeExtension ext, ExchangeRateState state) {
|
||
final rates = state.rates;
|
||
if (rates == null) return const SizedBox.shrink();
|
||
|
||
final popular = ExchangeRateService.popularCurrencies
|
||
.where((c) => c.code != state.fromCurrency && rates.containsKey(c.code))
|
||
.toList();
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(CupertinoIcons.chart_bar, size: 16, color: ext.accent),
|
||
const SizedBox(width: AppSpacing.xs),
|
||
Text(
|
||
'热门货币汇率',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Text(
|
||
'基于 1 ${state.fromCurrency}',
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: AppSpacing.sm),
|
||
Wrap(
|
||
spacing: AppSpacing.sm,
|
||
runSpacing: AppSpacing.sm,
|
||
children: popular.map((info) {
|
||
final rate = rates[info.code]!;
|
||
return _buildCurrencyChip(ext, info, rate, state);
|
||
}).toList(),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// ── 货币芯片 ──
|
||
Widget _buildCurrencyChip(
|
||
AppThemeExtension ext,
|
||
CurrencyInfo info,
|
||
double rate,
|
||
ExchangeRateState state,
|
||
) {
|
||
final isSelected = info.code == state.toCurrency;
|
||
final fmt = NumberFormat('#,##0.00##');
|
||
|
||
return GestureDetector(
|
||
onTap: () {
|
||
ref.read(exchangeRateProvider.notifier).setToCurrency(info.code);
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.sm,
|
||
vertical: AppSpacing.sm - 2,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: isSelected
|
||
? ext.accent.withValues(alpha: 0.12)
|
||
: ext.bgSecondary,
|
||
borderRadius: AppRadius.mdBorder,
|
||
border: isSelected
|
||
? Border.all(color: ext.accent, width: 1.2)
|
||
: null,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(info.flag, style: const TextStyle(fontSize: 14)),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
info.code,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: isSelected ? ext.accent : ext.textPrimary,
|
||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
fmt.format(rate),
|
||
style: AppTypography.caption2.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// ── 货币选择器弹窗 ──
|
||
void _showCurrencyPicker({
|
||
required bool isFrom,
|
||
required String currentCode,
|
||
}) {
|
||
final ext = AppTheme.ext(context);
|
||
|
||
showCupertinoModalPopup<void>(
|
||
context: context,
|
||
builder: (ctx) {
|
||
return Container(
|
||
height: MediaQuery.of(ctx).size.height * 0.5,
|
||
decoration: BoxDecoration(
|
||
color: ext.bgElevated,
|
||
borderRadius: const BorderRadius.vertical(
|
||
top: Radius.circular(AppRadius.xl),
|
||
),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(AppSpacing.md),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'选择货币',
|
||
style: AppTypography.headline.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
CupertinoButton(
|
||
padding: EdgeInsets.zero,
|
||
onPressed: () => Navigator.of(ctx).pop(),
|
||
child: Text(
|
||
'完成',
|
||
style: AppTypography.subhead.copyWith(
|
||
color: ext.accent,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: CupertinoScrollbar(
|
||
child: ListView.builder(
|
||
itemCount: ExchangeRateService.popularCurrencies.length,
|
||
itemBuilder: (_, index) {
|
||
final info =
|
||
ExchangeRateService.popularCurrencies[index];
|
||
final isSelected = info.code == currentCode;
|
||
|
||
return CupertinoButton(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: AppSpacing.md,
|
||
vertical: AppSpacing.sm,
|
||
),
|
||
onPressed: () {
|
||
if (isFrom) {
|
||
ref
|
||
.read(exchangeRateProvider.notifier)
|
||
.setFromCurrency(info.code);
|
||
ref
|
||
.read(exchangeRateProvider.notifier)
|
||
.loadRates();
|
||
} else {
|
||
ref
|
||
.read(exchangeRateProvider.notifier)
|
||
.setToCurrency(info.code);
|
||
}
|
||
Navigator.of(ctx).pop();
|
||
},
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
info.flag,
|
||
style: const TextStyle(fontSize: 24),
|
||
),
|
||
const SizedBox(width: AppSpacing.sm),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
info.code,
|
||
style: AppTypography.body.copyWith(
|
||
color: ext.textPrimary,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
Text(
|
||
info.name,
|
||
style: AppTypography.caption1.copyWith(
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (isSelected)
|
||
Icon(
|
||
CupertinoIcons.checkmark_circle_fill,
|
||
color: ext.accent,
|
||
size: 22,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
/// ── 日期格式化 ──
|
||
String _formatDate(DateTime dt) {
|
||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
|
||
}
|
||
}
|