import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/top_toast.dart'; import '../../../data/models/asset/asset_statement.dart'; import '../../../providers/statement_provider.dart'; import '../../widgets/common/app_refresh_indicator.dart'; class AssetHistoryScreen extends ConsumerWidget { const AssetHistoryScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(statementProvider); final notifier = ref.read(statementProvider.notifier); final filterCache = ref.watch(statementFilterCacheProvider); ref.listen(statementProvider, (prev, next) { if (next.errorMessage != null && next.errorMessage != prev?.errorMessage) { final l10n = AppLocalizations.of(context)!; showTopToast(context, message: resolveProviderError(next.errorMessage!, l10n) ?? next.errorMessage!, backgroundColor: AppColors.fall); } }); return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.chevron_left, size: 28), onPressed: () => context.pop(), ), title: Text(AppLocalizations.of(context)!.fundRecord, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), centerTitle: true, ), body: Column( children: [ _FilterSection( state: state, notifier: notifier, coins: filterCache.coins, types: filterCache.types, ), Expanded( child: _buildList(context, state, notifier, filterCache.types), ), ], ), ); } Widget _buildList( BuildContext context, StatementState state, StatementNotifier notifier, List types, ) { final cs = Theme.of(context).colorScheme; if (state.isLoading && state.records.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (state.records.isEmpty) { return Center( child: Text(AppLocalizations.of(context)!.noRecord, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 15)), ); } return AppRefreshIndicator( onRefresh: notifier.refresh, child: NotificationListener( onNotification: (notification) { if (notification is ScrollEndNotification && notification.metrics.pixels >= notification.metrics.maxScrollExtent - 100) { notifier.loadMore(); } return false; }, child: ListView.separated( padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), itemCount: state.records.length + (state.hasMore ? 1 : 0), separatorBuilder: (_, __) => const SizedBox(height: 10), itemBuilder: (_, i) { if (i >= state.records.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ); } return _RecordCard( record: state.records[i], types: types, coinCode: state.selectedCoinCode, ); }, ), ), ); } } Map _buildLocalTypeNames(AppLocalizations l10n) => { '0': l10n.txType0, '1': l10n.txType1, '2': l10n.txType2, '3': l10n.txType3, '4': l10n.txType4, '5': l10n.txType5, '6': l10n.txType6, '7': l10n.txType7, '8': l10n.txType8, '9': l10n.txType9, '10': l10n.txType10, '11': l10n.txType11, '13': l10n.txType13, '14': l10n.txType14, '15': l10n.txType15, '16': l10n.txType16, '17': l10n.txType17, '18': l10n.txType18, '19': l10n.txType19, '20': l10n.txType20, '21': l10n.txType21, '22': l10n.txType22, '23': l10n.txType23, '24': l10n.txType24, '25': l10n.txType25, '26': l10n.txType26, '27': l10n.txType27, '28': l10n.txType28, '29': l10n.txType29, '30': l10n.txType30, '31': l10n.txType31, '32': l10n.txType32, '33': l10n.txType33, '34': l10n.txType34, '35': l10n.txType35, '36': l10n.txType36, '37': l10n.txType37, '38': l10n.txType38, '39': l10n.txType39, '40': l10n.txType40, '41': l10n.txType41, '42': l10n.txType42, '43': l10n.txType43, '44': l10n.txType44, '45': l10n.txType45, '46': l10n.txType46, '47': l10n.txType47, '48': l10n.txType48, '49': l10n.txType49, '50': l10n.txType50, '51': l10n.txType51, }; // ══════════════════════════════════════════════════════════════ // 筛选区 // ══════════════════════════════════════════════════════════════ class _FilterSection extends StatelessWidget { const _FilterSection({ required this.state, required this.notifier, required this.coins, required this.types, }); final StatementState state; final StatementNotifier notifier; final List coins; final List types; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final dateFmt = DateFormat('yyyy-MM-dd'); final l10n = AppLocalizations.of(context)!; // 构建选项列表:(value, label) final coinOptions = <(String, String)>[ ('', l10n.all), ...coins.map((c) => (c.code, c.code)), ]; final localTypeNames = _buildLocalTypeNames(l10n); final typeOptions = <(String, String)>[ ('', l10n.all), ...types.map((t) => (t.typeId, localTypeNames[t.typeId] ?? t.name)), ]; final coinLabel = coinOptions .firstWhere((e) => e.$1 == state.selectedCoinCode, orElse: () => ('', l10n.all)) .$2; final typeLabel = typeOptions .firstWhere((e) => e.$1 == state.selectedTypeId, orElse: () => ('', l10n.all)) .$2; return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 币种 + 类型 Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(l10n.coinLabel, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12)), const SizedBox(height: 6), _PickerField( label: coinLabel, items: coinOptions, value: state.selectedCoinCode, title: l10n.selectCoin, onChanged: (v) => notifier.selectCoin(v), ), ], ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(l10n.typeLabel, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12)), const SizedBox(height: 6), _PickerField( label: typeLabel, items: typeOptions, value: state.selectedTypeId, title: l10n.selectType, onChanged: (v) => notifier.selectType(v), ), ], ), ), ], ), const SizedBox(height: 12), // 起止时间 + 重置/搜索 Text(l10n.timeRange, style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12)), const SizedBox(height: 6), Row( children: [ Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), decoration: BoxDecoration( color: isDark ? AppColors.darkBgTertiary : AppColors.lightBgSecondary, border: Border.all( color: isDark ? AppColors.darkCardBorder.withAlpha(180) : AppColors.lightBorder), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Flexible( child: _TimeChip( label: state.startDate != null ? dateFmt.format(state.startDate!) : l10n.startTime, onTap: () async { final date = await _showDrumDatePicker( context: context, initialDate: state.startDate ?? DateTime.now(), firstDate: DateTime(2017), lastDate: DateTime.now(), title: l10n.startTime, ); if (date != null) notifier.selectStartDate(date); }, ), ), Text('~', style: TextStyle( color: cs.onSurface.withAlpha(120))), Flexible( child: _TimeChip( label: state.endDate != null ? dateFmt.format(state.endDate!) : l10n.endTime, onTap: () async { final date = await _showDrumDatePicker( context: context, initialDate: state.endDate ?? DateTime.now(), firstDate: DateTime(2017), lastDate: DateTime.now(), title: l10n.endTime, ); if (date != null) notifier.selectEndDate(date); }, ), ), ], ), ), ), const SizedBox(width: 8), GestureDetector( onTap: notifier.reset, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), decoration: BoxDecoration( border: Border.all( color: isDark ? AppColors.darkCardBorder.withAlpha(180) : AppColors.lightBorder), borderRadius: BorderRadius.circular(8), ), child: Text(l10n.reset, style: TextStyle(color: cs.onSurface, fontSize: 13)), ), ), const SizedBox(width: 8), GestureDetector( onTap: notifier.search, child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 9), decoration: BoxDecoration( color: AppColors.brand, borderRadius: BorderRadius.circular(8), ), child: Text(l10n.search, style: const TextStyle( color: Colors.black, fontSize: 13, fontWeight: FontWeight.w600)), ), ), ], ), ], ), ); } } // ══════════════════════════════════════════════════════════════ // 自定义选择器字段(替代 DropdownButtonFormField) // ══════════════════════════════════════════════════════════════ class _PickerField extends StatelessWidget { const _PickerField({ required this.label, required this.items, required this.value, required this.title, required this.onChanged, }); final String label; final List<(String, String)> items; // (value, displayLabel) final String value; final String title; final ValueChanged onChanged; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final fieldBg = isDark ? AppColors.darkBgTertiary : AppColors.lightBgSecondary; final borderColor = isDark ? AppColors.darkCardBorder.withAlpha(180) : AppColors.lightBorder; return LayoutBuilder( builder: (_, constraints) => GestureDetector( onTap: () => _showSheet(context), child: Container( width: constraints.maxWidth, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11), decoration: BoxDecoration( color: fieldBg, border: Border.all(color: borderColor), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( child: Text( label, style: TextStyle(color: cs.onSurface, fontSize: 14), overflow: TextOverflow.ellipsis, ), ), Icon(Icons.keyboard_arrow_down, size: 18, color: cs.onSurface.withAlpha(120)), ], ), ), ), ); } void _showSheet(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: isDark ? AppColors.darkBgSecondary : AppColors.lightBg, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) { final mq = MediaQuery.of(ctx); return LayoutBuilder( builder: (ctx, constraints) { // 把手 + 标题 + 分隔线 + 底安全区,与下方列表分离避免超出 BottomSheet 高度 const topReserve = 100.0; final bottomGap = mq.padding.bottom + 8; final parentH = constraints.maxHeight; final listMax = parentH.isFinite ? math.min( mq.size.height * 0.45, math.max(120.0, parentH - topReserve - bottomGap), ) : mq.size.height * 0.45; return Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only(top: 12, bottom: 4), child: Container( width: 36, height: 4, decoration: BoxDecoration( color: cs.onSurface.withAlpha(40), borderRadius: BorderRadius.circular(2), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: Text(title, style: TextStyle( color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)), ), Divider(height: 1, color: cs.outline.withAlpha(40)), ConstrainedBox( constraints: BoxConstraints(maxHeight: listMax), child: ListView.separated( shrinkWrap: true, itemCount: items.length, separatorBuilder: (_, __) => Divider(height: 1, color: cs.outline.withAlpha(25)), itemBuilder: (_, i) { final (val, lbl) = items[i]; final isSelected = val == value; return GestureDetector( onTap: () { Navigator.pop(ctx); onChanged(val); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 14), child: Row( children: [ Expanded( child: Text( lbl, style: TextStyle( color: isSelected ? AppColors.brand : cs.onSurface, fontSize: 15, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), ), ), if (isSelected) const Icon(Icons.check, size: 18, color: AppColors.brand), ], ), ), ); }, ), ), SizedBox(height: bottomGap), ], ); }, ); }, ); } } // ══════════════════════════════════════════════════════════════ // 流水记录卡片 // ══════════════════════════════════════════════════════════════ /// 类别行展示:去掉服务端固定的中文「永续」或英文 perpetual,改为 [AppLocalizations.perpetual]。 String localizedStatementCategorySymbol(String raw, AppLocalizations l10n) { final s = raw.trim(); if (s.isEmpty) { return s; } final zh = RegExp(r'\s*永续\s*$'); if (zh.hasMatch(s)) { final base = s.replaceFirst(zh, '').trim(); if (base.isEmpty) { return l10n.perpetual; } return '$base ${l10n.perpetual}'; } final en = RegExp(r'\s+perpetual\s*$', caseSensitive: false); if (en.hasMatch(s)) { final base = s.replaceFirst(en, '').trim(); if (base.isEmpty) { return l10n.perpetual; } return '$base ${l10n.perpetual}'; } return s; } /// 现货成交(type=48)类别不与永续挂钩:去掉尾部永续表述,仅保留标的(与 Web 一致)。 String localizedStatementCategoryForRecord( AssetStatement record, AppLocalizations l10n, ) { if (record.type == '48') { return _stripPerpetualSuffixRaw(record.displaySymbol); } return localizedStatementCategorySymbol(record.displaySymbol, l10n); } String _stripPerpetualSuffixRaw(String raw) { final s = raw.trim(); if (s.isEmpty) { return s; } var t = s.replaceFirst(RegExp(r'\s*永续\s*$'), '').trim(); t = t.replaceFirst(RegExp(r'\s+perpetual\s*$', caseSensitive: false), '').trim(); t = t.replaceFirst(RegExp(r'\s*無期限\s*$'), '').trim(); if (t.isEmpty) { return s.split(RegExp(r'\s+')).first; } return t; } class _RecordCard extends StatelessWidget { const _RecordCard({ required this.record, required this.types, this.coinCode = '', }); final AssetStatement record; final List types; final String coinCode; String _localizedExtraTitle(AppLocalizations l10n) { switch (record.type) { case '1': return l10n.feeUsdt; case '19': return l10n.openCloseLabel; case '25': return l10n.rebateId; default: return ''; } } String _localizedExtraValue(AppLocalizations l10n) { switch (record.type) { case '1': return record.fee; case '19': return record.direction == '0' ? l10n.openPosition : l10n.closePosition; case '25': return record.rebateId; default: return ''; } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final amount = double.tryParse(record.amount) ?? 0; final isPositive = amount >= 0; final amountColor = isPositive ? AppColors.rise : AppColors.fall; final amountStr = isPositive ? '+${_formatAmount(amount)}' : _formatAmount(amount); final l10n = AppLocalizations.of(context)!; final typeName = _findTypeName(record.type, l10n); final coin = coinCode.isNotEmpty ? coinCode : 'USDT'; // 数量标签用记录本身的币种(取 symbol 第一个词,如 "BTC 永续" → "BTC") final amountCoin = record.symbol.isNotEmpty ? record.symbol.split(' ').first : coin; final timeParts = record.createTime.split(' '); final datePart = timeParts.isNotEmpty ? timeParts[0] : record.createTime; final timePart = timeParts.length > 1 ? timeParts[1] : ''; final hasExtra = record.showExtraField || record.symbol.isNotEmpty; // 颜色分层:header背景 vs body背景 final headerBg = isDark ? AppColors.brand.withAlpha(90) : AppColors.tagIndigoBgLight; final bodyBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg; final borderColor = isDark ? AppColors.darkCardBorder : AppColors.lightBorder; return Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: 0.5), color: bodyBg, // 封住底部 + 为 Divider 提供背景色 ), child: Column( children: [ // ── Header:币种 + 已完成(带背景色)────────── Container( color: headerBg, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), child: Row( children: [ Text( 'USDT', style: TextStyle( color: cs.onSurface, fontSize: 14, fontWeight: FontWeight.w600, ), ), const Spacer(), Text( AppLocalizations.of(context)!.completed, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 12, ), ), ], ), ), // ── Body ──────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 类型 Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.typeLabel, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 11)), const SizedBox(height: 5), Text( typeName, style: TextStyle( color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500, ), ), ], ), ), // 时间 Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.timeLabel, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 11)), const SizedBox(height: 5), Text(datePart, style: TextStyle( color: cs.onSurface, fontSize: 12)), if (timePart.isNotEmpty) Text(timePart, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 12)), ], ), ), // 数量(右对齐) Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${AppLocalizations.of(context)!.amountLabel}($amountCoin)', overflow: TextOverflow.ellipsis, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 11)), const SizedBox(height: 5), Text( amountStr, overflow: TextOverflow.ellipsis, style: TextStyle( color: amountColor, fontSize: 14, fontWeight: FontWeight.w600, fontFeatures: const [FontFeature.tabularFigures()], ), ), ], ), ), ], ), ), // ── 额外字段行(类别 / 开平仓)────────────── if (hasExtra) ...[ Divider( height: 1, thickness: 0.5, color: borderColor, indent: 14, endIndent: 14, ), Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 12), child: Row( children: [ if (record.symbol.isNotEmpty) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.categoryLabel, style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 11)), const SizedBox(height: 5), Text(localizedStatementCategoryForRecord(record, l10n), style: TextStyle( color: cs.onSurface, fontSize: 13)), ], ), ), if (record.showExtraField) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_localizedExtraTitle(l10n), style: TextStyle( color: cs.onSurface.withAlpha(100), fontSize: 11)), const SizedBox(height: 5), Text(_localizedExtraValue(l10n), style: TextStyle( color: cs.onSurface, fontSize: 13)), ], ), ), ], ), ), ], ], ), ); } Map _localTypeNames(AppLocalizations l10n) => _buildLocalTypeNames(l10n); String _findTypeName(String typeId, AppLocalizations l10n) { final localName = _localTypeNames(l10n)[typeId]; if (localName != null) return localName; for (final t in types) { if (t.typeId == typeId) return t.name; } return typeId; } String _formatAmount(double value) { final abs = value.abs(); int decimals; if (abs < 0.0001) { decimals = 8; } else if (abs < 0.001) { decimals = 7; } else if (abs < 0.01) { decimals = 6; } else if (abs < 0.1) { decimals = 5; } else if (abs < 1) { decimals = 4; } else { decimals = 2; } final factor = _pow10(decimals); final truncated = (value * factor).truncateToDouble() / factor; var str = truncated.toStringAsFixed(decimals); if (str.contains('.')) { str = str.replaceAll(RegExp(r'0+$'), ''); str = str.replaceAll(RegExp(r'\.$'), ''); } return str; } double _pow10(int n) { double result = 1; for (int i = 0; i < n; i++) result *= 10; return result; } } // ══════════════════════════════════════════════════════════════ // 时间选择 Chip // ══════════════════════════════════════════════════════════════ class _TimeChip extends StatelessWidget { const _TimeChip({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 5), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.access_time_outlined, size: 13, color: cs.onSurface.withAlpha(100)), const SizedBox(width: 4), Flexible( child: Text( label, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 12), overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } } // ══════════════════════════════════════════════════════════════ // 滚筒日期选择器 // ══════════════════════════════════════════════════════════════ Future _showDrumDatePicker({ required BuildContext context, required DateTime initialDate, required DateTime firstDate, required DateTime lastDate, String? title, }) { final isDark = Theme.of(context).brightness == Brightness.dark; final resolvedTitle = title ?? AppLocalizations.of(context)!.selectDate; return showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (ctx) => _DrumDatePickerSheet( initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, title: resolvedTitle, isDark: isDark, ), ); } class _DrumDatePickerSheet extends StatefulWidget { const _DrumDatePickerSheet({ required this.initialDate, required this.firstDate, required this.lastDate, required this.title, required this.isDark, }); final DateTime initialDate; final DateTime firstDate; final DateTime lastDate; final String title; final bool isDark; @override State<_DrumDatePickerSheet> createState() => _DrumDatePickerSheetState(); } class _DrumDatePickerSheetState extends State<_DrumDatePickerSheet> { late int _year; late int _month; late int _day; late List _years; late FixedExtentScrollController _yearCtrl; late FixedExtentScrollController _monthCtrl; late FixedExtentScrollController _dayCtrl; @override void initState() { super.initState(); _years = List.generate( widget.lastDate.year - widget.firstDate.year + 1, (i) => widget.firstDate.year + i, ); _year = widget.initialDate.year.clamp(widget.firstDate.year, widget.lastDate.year); _month = widget.initialDate.month; _day = widget.initialDate.day.clamp(1, _daysInMonth); _yearCtrl = FixedExtentScrollController(initialItem: _year - widget.firstDate.year); _monthCtrl = FixedExtentScrollController(initialItem: _month - 1); _dayCtrl = FixedExtentScrollController(initialItem: _day - 1); } @override void dispose() { _yearCtrl.dispose(); _monthCtrl.dispose(); _dayCtrl.dispose(); super.dispose(); } int get _daysInMonth => DateTime(_year, _month + 1, 0).day; void _onYearChanged(int index) { setState(() { _year = _years[index]; _clampDay(); }); } void _onMonthChanged(int index) { setState(() { _month = index + 1; _clampDay(); }); } void _onDayChanged(int index) { setState(() { _day = (index + 1).clamp(1, _daysInMonth); }); } void _clampDay() { final maxDay = _daysInMonth; if (_day > maxDay) { _day = maxDay; WidgetsBinding.instance.addPostFrameCallback((_) { if (_dayCtrl.hasClients) _dayCtrl.jumpToItem(_day - 1); }); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final sheetBg = widget.isDark ? const Color(0xFF1C2128) : Colors.white; final highlightBg = widget.isDark ? AppColors.darkBgTertiary : AppColors.lightBgSecondary; final gradientColor = widget.isDark ? const Color(0xFF1C2128) : Colors.white; final dividerColor = widget.isDark ? Colors.white.withAlpha(20) : const Color(0xFFF0F0F0); final textStyle = TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface); final preview = '$_year-${_month.toString().padLeft(2, '0')}-${_day.toString().padLeft(2, '0')}'; return Container( decoration: BoxDecoration( color: sheetBg, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: SafeArea( child: LayoutBuilder( builder: (context, constraints) { final mq = MediaQuery.of(context); // SafeArea 已处理底部;此处为工具栏 + 预览 + 分隔线 + 底留白 const fixedToolbar = 50.0 + 44.0 + 2.0 + 8.0; final maxH = constraints.maxHeight.isFinite ? constraints.maxHeight : mq.size.height * 0.42; final wheelH = (maxH - fixedToolbar).clamp(160.0, 240.0); return Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 50, child: Row( children: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(AppLocalizations.of(context)!.cancel, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: cs.onSurface.withAlpha(153))), ), Expanded( child: Text(widget.title, textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface)), ), TextButton( onPressed: () => Navigator.pop(context, DateTime(_year, _month, _day)), child: Text(AppLocalizations.of(context)!.confirm, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.brand)), ), ], ), ), Divider(height: 1, color: dividerColor), // ── 预览行 ────────────────────────────────────── SizedBox( height: 44, child: Center( child: Text(preview, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.brand)), ), ), Divider(height: 1, color: dividerColor), SizedBox( height: wheelH, child: Stack( children: [ // 选中行高亮背景 Positioned.fill( child: Center( child: Container( height: 44, margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: highlightBg, borderRadius: BorderRadius.circular(8), ), ), ), ), // 三列 Row( children: [ // 年份列 Expanded( flex: 5, child: ListWheelScrollView.useDelegate( controller: _yearCtrl, itemExtent: 44, physics: const FixedExtentScrollPhysics(), onSelectedItemChanged: _onYearChanged, childDelegate: ListWheelChildBuilderDelegate( childCount: _years.length, builder: (_, i) => Center( child: Text('${_years[i]}${AppLocalizations.of(context)!.yearSuffix}', style: textStyle), ), ), ), ), // 月份列 Expanded( flex: 3, child: ListWheelScrollView.useDelegate( controller: _monthCtrl, itemExtent: 44, physics: const FixedExtentScrollPhysics(), onSelectedItemChanged: _onMonthChanged, childDelegate: ListWheelChildBuilderDelegate( childCount: 12, builder: (_, i) => Center( child: Text('${i + 1}${AppLocalizations.of(context)!.monthSuffix}', style: textStyle), ), ), ), ), // 日期列 Expanded( flex: 3, child: ListWheelScrollView.useDelegate( controller: _dayCtrl, itemExtent: 44, physics: const FixedExtentScrollPhysics(), onSelectedItemChanged: _onDayChanged, childDelegate: ListWheelChildBuilderDelegate( childCount: _daysInMonth, builder: (_, i) => Center( child: Text('${i + 1}${AppLocalizations.of(context)!.daySuffix}', style: textStyle), ), ), ), ), ], ), // 上渐变遮罩 Positioned( top: 0, left: 0, right: 0, height: 88, child: IgnorePointer( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [gradientColor, gradientColor.withAlpha(0)], ), ), ), ), ), // 下渐变遮罩 Positioned( bottom: 0, left: 0, right: 0, height: 88, child: IgnorePointer( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [gradientColor, gradientColor.withAlpha(0)], ), ), ), ), ), ], ), ), const SizedBox(height: 8), ], ); }, ), ), ); } }