// screens/license/license_orders_tab.dart — 授权购买订单管理 tab // (原型 license.html #tab-orders / m-license.html #tab-orders)。 // 数据源:providers/license_purchase_provider.dart。后端 GET /license/purchases // 只接受 page/page_size/status 三个查询轴;套餐/关键词/下单时间筛选走客户端 // 过滤(当前页),与 stock_out 列表「仓库」筛选同一 established 口径。 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/exceptions.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/app_dims.g.dart'; import '../../core/theme/app_fonts.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/utils/clock.dart'; import '../../core/utils/money.dart'; import '../../models/license.dart'; import '../../providers/license_provider.dart' show licenseProvider, licenseRepositoryProvider; import '../../providers/license_purchase_provider.dart'; import '../../widgets/order_detail_drawer.dart' show showOrderDetailDrawer; import '../../widgets/ds/ds_atoms.dart'; import '../../widgets/ds/ds_kpi.dart'; import '../../widgets/ds/ds_menu.dart'; import '../../widgets/ds/ds_table.dart'; import '../../widgets/ds/ds_toast.dart'; import '../../widgets/ds/m_card.dart'; import '../../widgets/ds/m_kpi_grid.dart'; import '../../widgets/ds/m_sheet.dart'; import '../../widgets/ds/status_icon_map.dart' show statusIcon; import '../../widgets/wheel_date_picker.dart' show showDateRangeDropdown; import '../../widgets/write_guard.dart'; // bizCode → (展示名, 授权天数):与 core/config/license_plans.dart 的套餐表同源。 const Map _kPlanLabels = { 'promo_first_month': ('首月特惠 · 30 天', 30), 'monthly_standard': ('标准版 · 月付', 30), 'annual_standard': ('标准版 · 年付', 365), 'monthly_pro': ('高级版 · 月付', 30), 'annual_pro': ('高级版 · 年付', 365), }; String _planLabel(String bizCode) => _kPlanLabels[bizCode]?.$1 ?? bizCode; int? _planDays(String bizCode) => _kPlanLabels[bizCode]?.$2; // 状态三态与后端对齐:pending=待支付 / paid=已支付 / failed=已关闭(取消·超时·失败归并)。 const _kStatusLabels = {'pending': '待支付', 'paid': '已支付', 'failed': '已关闭'}; String _statusLabel(String status) => _kStatusLabels[status] ?? status; DsBadgeTone _statusTone(String status) => switch (status) { 'paid' => DsBadgeTone.ok, 'pending' => DsBadgeTone.warn, _ => DsBadgeTone.muted, }; class LicenseOrdersTab extends ConsumerStatefulWidget { /// 详情面板「查看授权」→ 切回授权信息 tab。 final VoidCallback onGoInfo; /// 详情面板「重新购买」→ 切到购买续费 tab;null 表示购买 tab 当前不可用 /// (iOS 合规态整 tab 隐藏 / 非管理员),此时不渲染该按钮。 final VoidCallback? onGoBuy; const LicenseOrdersTab({super.key, required this.onGoInfo, this.onGoBuy}); @override ConsumerState createState() => _LicenseOrdersTabState(); } class _LicenseOrdersTabState extends ConsumerState { final _searchCtrl = TextEditingController(); String _query = ''; String _plan = ''; // '' = 全部,否则 bizCode String _status = ''; // '' = 全部,否则 pending/paid/failed(同步服务端) DateTimeRange? _dateRange; String _datePresetLabel = ''; @override void initState() { super.initState(); _status = ref.read(purchaseListProvider.notifier).currentStatus; } @override void dispose() { _searchCtrl.dispose(); super.dispose(); } bool get _hasAnyFilter => _query.isNotEmpty || _plan.isNotEmpty || _status.isNotEmpty || _dateRange != null; void _resetFilters() { setState(() { _searchCtrl.clear(); _query = ''; _plan = ''; _status = ''; _dateRange = null; _datePresetLabel = ''; }); ref.read(purchaseListProvider.notifier).setStatus(''); } void _setStatus(String status) { setState(() => _status = status); ref.read(purchaseListProvider.notifier).setStatus(status); } List _filtered(List items) { return items.where((r) { if (_plan.isNotEmpty && r.productBizCode != _plan) return false; if (_dateRange != null) { final d = r.createdAt; if (d == null) return false; final day = DateTime(d.year, d.month, d.day); final start = DateTime(_dateRange!.start.year, _dateRange!.start.month, _dateRange!.start.day); final end = DateTime( _dateRange!.end.year, _dateRange!.end.month, _dateRange!.end.day); if (day.isBefore(start) || day.isAfter(end)) return false; } if (_query.isNotEmpty) { final q = _query.toLowerCase(); if (!r.outTradeNo.toLowerCase().contains(q) && !_planLabel(r.productBizCode).toLowerCase().contains(q)) { return false; } } return true; }).toList(); } @override Widget build(BuildContext context) { final async = ref.watch(purchaseListProvider); return async.when( skipLoadingOnReload: true, loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.cloudOff, size: 40, color: context.tokens.muted), const SizedBox(height: 12), Text('加载失败:$e', style: TextStyle(color: context.tokens.muted), textAlign: TextAlign.center), const SizedBox(height: 12), DsButton('重试', variant: DsBtnVariant.primary, onPressed: () => ref.read(purchaseListProvider.notifier).reload()), ], ), ), data: (result) => Stack(children: [ _buildLoaded(result), if (async.isLoading) const Positioned.fill(child: DsLoadingScrim()), ]), ); } Widget _buildLoaded(PurchaseListResult result) { final mobile = context.isMobile; final rows = _filtered(result.items); final notifier = ref.read(purchaseListProvider.notifier); if (mobile) { return Column( children: [ _buildMobileKpis(result.summary), _buildMobileSearchRow(), _buildMobileSection(rows.length), Expanded( child: DsTable( total: result.total, page: notifier.page, pageSize: notifier.pageSize, onPageChanged: (p) => notifier.setPage(p), onPageSizeChanged: (s) => notifier.setPageSize(s), emptyText: '没有匹配的授权订单', columns: _columns, rows: const [], mobileCards: rows.map(_orderCard).toList(), onRefresh: () async { notifier.reload(); await Future.delayed(const Duration(milliseconds: 600)); }, ), ), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildKpis(result.summary), const SizedBox(height: 20), Expanded( child: DsTable( total: result.total, page: notifier.page, pageSize: notifier.pageSize, onPageChanged: (p) => notifier.setPage(p), onPageSizeChanged: (s) => notifier.setPageSize(s), toolbar: _buildToolbar(), emptyText: '没有匹配的授权订单 · 试试调整筛选或搜索', columns: _columns, rows: rows.map(_row).toList(), ), ), ], ); } // ── KPI(原型 .kpis 4 卡)──────────────────────────────────────── Widget _buildKpis(PurchaseSummary summary) { final lic = ref.watch(licenseProvider).valueOrNull; final expires = lic?.expiresAt != null ? DateFormat('yyyy-MM-dd').format(lic!.expiresAt!) : '—'; final days = lic?.expiresAt != null ? lic!.expiresAt!.difference(appNow()).inDays.clamp(0, 99999) : null; final cards = [ DsKpi( title: '当前授权 · ${lic?.typeLabel ?? "—"}', value: expires, icon: LucideIcons.shieldCheck, tone: DsKpiTone.ok, delta: days != null ? '剩余 $days 天 · 续期自动叠加' : '未激活', deltaTone: DsKpiDelta.up, ), DsKpi( title: '累计购买金额', value: '¥${yuanFromMinor(summary.paidTotalMinor)}', icon: LucideIcons.banknote, tone: DsKpiTone.info, delta: '已支付 ${summary.paidCount} 笔', ), DsKpi( title: '订单总数', value: '${summary.totalCount}', icon: LucideIcons.receiptText, tone: DsKpiTone.info, ), DsKpi( title: '待支付 · 点击筛选', value: '${summary.pendingCount}', icon: LucideIcons.clock, tone: DsKpiTone.warn, delta: summary.pendingCount > 0 ? '未支付订单 2 小时后自动关闭' : '点击筛选', onTap: () => _setStatus(_status == 'pending' ? '' : 'pending'), ), ]; return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < cards.length; i++) ...[ if (i > 0) const SizedBox(width: AppDims.sp3), Expanded(child: cards[i]), ], ], ), ); } Widget _buildMobileKpis(PurchaseSummary summary) { final lic = ref.watch(licenseProvider).valueOrNull; final expires = lic?.expiresAt != null ? DateFormat('yyyy-MM-dd').format(lic!.expiresAt!) : '—'; final days = lic?.expiresAt != null ? lic!.expiresAt!.difference(appNow()).inDays.clamp(0, 99999) : null; return Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 0), child: MKpiGrid(items: [ MKpiItem( label: '当前授权 · ${lic?.typeLabel ?? "—"}', value: expires, icon: LucideIcons.shieldCheck, delta: days != null ? '剩余 $days 天' : '未激活', deltaTone: MKpiDeltaTone.up, ), MKpiItem( label: '累计购买金额', value: '¥${yuanFromMinor(summary.paidTotalMinor)}', icon: LucideIcons.banknote, delta: '已支付 ${summary.paidCount} 笔', ), MKpiItem( label: '待支付 · 点击筛选', value: '${summary.pendingCount}', icon: LucideIcons.clock, delta: summary.pendingCount > 0 ? '2 小时后自动关闭' : '点击筛选', deltaTone: summary.pendingCount > 0 ? MKpiDeltaTone.warn : MKpiDeltaTone.normal, selected: _status == 'pending', onTap: () => _setStatus(_status == 'pending' ? '' : 'pending'), ), MKpiItem( label: '订单总数', value: '${summary.totalCount}', icon: LucideIcons.receiptText, ), ]), ); } // ── 桌面工具栏(原型 .toolbar)─────────────────────────────────── Widget _buildToolbar() { final searchField = SizedBox( width: 260, child: DsSearchBox( controller: _searchCtrl, hint: '搜索订单号 / 套餐', onChanged: (v) => setState(() => _query = v.trim()), ), ); final planChip = Builder( builder: (anchorCtx) => DsChip( label: '套餐', value: _plan.isEmpty ? null : _planLabel(_plan), onTap: () async { final picked = await showDsMenu(anchorCtx, items: [ DsMenuItem(value: '', label: '全部', selected: _plan.isEmpty), for (final code in _kPlanLabels.keys) DsMenuItem( value: code, label: _planLabel(code), selected: code == _plan), ]); if (picked != null) setState(() => _plan = picked); }, onClear: () => setState(() => _plan = ''), ), ); final statusChip = Builder( builder: (anchorCtx) => DsChip( label: '状态', value: _status.isEmpty ? null : _statusLabel(_status), onTap: () async { final picked = await showDsMenu(anchorCtx, items: [ DsMenuItem(value: '', label: '全部', selected: _status.isEmpty), for (final s in _kStatusLabels.keys) DsMenuItem( value: s, label: _statusLabel(s), selected: s == _status), ]); if (picked != null) _setStatus(picked); }, onClear: () => _setStatus(''), ), ); final dateChip = Builder( builder: (anchorCtx) => DsChip( label: '下单时间', value: _datePresetLabel.isEmpty ? null : _datePresetLabel, onTap: () => _pickDatePreset(anchorCtx), onClear: () => setState(() { _dateRange = null; _datePresetLabel = ''; }), ), ); final resetBtn = DsButton( '重置', icon: LucideIcons.refreshCw, small: true, variant: _hasAnyFilter ? DsBtnVariant.primary : DsBtnVariant.ghost, onPressed: _resetFilters, ); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ searchField, const SizedBox(width: AppDims.sp3), Expanded( child: Wrap( spacing: AppDims.sp2, runSpacing: AppDims.sp2, crossAxisAlignment: WrapCrossAlignment.center, children: [planChip, statusChip, dateChip], ), ), const SizedBox(width: AppDims.sp2), resetBtn, ], ); } Future _pickDatePreset(BuildContext anchorCtx) async { final picked = await showDsMenu(anchorCtx, items: [ DsMenuItem(value: 'all', label: '全部时间', selected: _datePresetLabel.isEmpty), const DsMenuItem(value: '7', label: '近 7 天'), const DsMenuItem(value: '30', label: '近 30 天'), const DsMenuItem(value: 'month', label: '本月'), const DsMenuItem(value: 'custom', label: '自定义…'), ]); if (picked == null) return; if (picked == 'custom') { if (!anchorCtx.mounted) return; final range = await showDateRangeDropdown(anchorCtx, initial: _dateRange); if (range == null) return; setState(() { _dateRange = range; _datePresetLabel = '${DateFormat('yyyy-MM-dd').format(range.start)} ' '~ ${DateFormat('yyyy-MM-dd').format(range.end)}'; }); return; } final now = appNow(); DateTimeRange? range; String label = ''; switch (picked) { case '7': range = DateTimeRange(start: now.subtract(const Duration(days: 6)), end: now); label = '近 7 天'; break; case '30': range = DateTimeRange( start: now.subtract(const Duration(days: 29)), end: now); label = '近 30 天'; break; case 'month': range = DateTimeRange(start: DateTime(now.year, now.month, 1), end: now); label = '本月'; break; default: range = null; label = ''; } setState(() { _dateRange = range; _datePresetLabel = label; }); } // ── 移动搜索行(原型:搜索框 + 状态钮 + 套餐钮)─────────────────── Widget _buildMobileSearchRow() { return Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 0), child: Row(children: [ Expanded( child: DsSearchBox( controller: _searchCtrl, hint: '搜索订单号 / 套餐', onChanged: (v) => setState(() => _query = v.trim()), ), ), const SizedBox(width: 8), DsButton(_status.isEmpty ? '全部' : _statusLabel(_status), small: true, variant: _status.isEmpty ? DsBtnVariant.ghost : DsBtnVariant.primary, onPressed: _openStatusSheet), const SizedBox(width: 8), DsButton(_plan.isEmpty ? '套餐' : _planLabel(_plan), small: true, variant: _plan.isEmpty ? DsBtnVariant.ghost : DsBtnVariant.primary, onPressed: _openPlanSheet), ]), ); } Widget _buildMobileSection(int count) { return Padding( padding: const EdgeInsets.fromLTRB(14, 14, 14, 8), child: Align( alignment: Alignment.centerLeft, child: Text('授权订单 · 共 $count', style: TextStyle( fontSize: AppDims.fsSm, fontWeight: FontWeight.w700, letterSpacing: .4, color: context.tokens.muted)), ), ); } Future _openStatusSheet() async { final options = <(String, String)>[ ('', '全部'), ('pending', '待支付'), ('paid', '已支付'), ('failed', '已关闭'), ]; final sel = await showMSheet( context, title: '状态筛选', builder: (ctx) { final t = ctx.tokens; return Column( mainAxisSize: MainAxisSize.min, children: [ for (var i = 0; i < options.length; i++) InkWell( onTap: () => Navigator.of(ctx).pop(options[i].$1), child: Container( padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4), decoration: BoxDecoration( border: i < options.length - 1 ? Border(bottom: BorderSide(color: t.borderSubtle)) : null, ), child: Row(children: [ if (options[i].$2 != '全部') ...[ DsIconBadge(options[i].$2), const SizedBox(width: 10), ], Text(options[i].$2, style: TextStyle( fontSize: AppDims.fsBody, fontWeight: options[i].$1 == _status ? FontWeight.w600 : FontWeight.w400, color: options[i].$1 == _status ? t.primary : t.text)), const Spacer(), if (options[i].$1 == _status) Icon(LucideIcons.check, size: 18, color: t.primary), ]), ), ), ], ); }, ); if (sel == null || !mounted) return; _setStatus(sel); } Future _openPlanSheet() async { final options = <(String, String)>[ ('', '全部'), for (final code in _kPlanLabels.keys) (code, _planLabel(code)), ]; final sel = await showMSheet( context, title: '套餐筛选', builder: (ctx) { final t = ctx.tokens; return Column( mainAxisSize: MainAxisSize.min, children: [ for (var i = 0; i < options.length; i++) InkWell( onTap: () => Navigator.of(ctx).pop(options[i].$1), child: Container( padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4), decoration: BoxDecoration( border: i < options.length - 1 ? Border(bottom: BorderSide(color: t.borderSubtle)) : null, ), child: Row(children: [ Expanded( child: Text(options[i].$2, style: TextStyle( fontSize: AppDims.fsBody, fontWeight: options[i].$1 == _plan ? FontWeight.w600 : FontWeight.w400, color: options[i].$1 == _plan ? t.primary : t.text)), ), if (options[i].$1 == _plan) Icon(LucideIcons.check, size: 18, color: t.primary), ]), ), ), ], ); }, ); if (sel == null || !mounted) return; setState(() => _plan = sel); } // ── 表格列 / 行(桌面)─────────────────────────────────────────── static const _columns = [ DsColumn('no', '订单号'), DsColumn('plan', '套餐'), DsColumn('days', '时长', numeric: true), DsColumn('amount', '金额', numeric: true), DsColumn('status', '状态'), DsColumn('by', '下单人'), DsColumn('created', '下单时间'), DsColumn('paid', '支付时间'), DsColumn('actions', '操作', action: true), ]; DsRow _row(PurchaseRecord r) { final t = context.tokens; final days = _planDays(r.productBizCode); return DsRow( onTap: () => _showDetail(r), cells: [ Text(r.outTradeNo, style: TextStyle( color: t.faint, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: AppDims.fsSm)), Text(_planLabel(r.productBizCode)), Text(days != null ? '$days 天' : '—'), Text('¥${r.displayAmount}', style: const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback)), _statusBadge(r.status), Text(r.userName.isEmpty ? '—' : r.userName), Text( r.createdAt != null ? DateFormat('yyyy-MM-dd HH:mm').format(r.createdAt!) : '—', style: TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: AppDims.fsSm)), Text( r.paidAt != null ? DateFormat('yyyy-MM-dd HH:mm').format(r.paidAt!) : '—', style: TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: AppDims.fsSm, color: r.paidAt == null ? t.faint : null)), IconButton( icon: Icon(LucideIcons.eye, size: 16, color: t.muted), tooltip: '详情', splashRadius: 18, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), padding: EdgeInsets.zero, onPressed: () => _showDetail(r), ), ], ); } Widget _statusBadge(String status) => DsBadge(_statusLabel(status), tone: _statusTone(status), icon: statusIcon(_statusLabel(status))); MCard _orderCard(PurchaseRecord r) { final createdText = r.createdAt != null ? DateFormat('yyyy-MM-dd HH:mm').format(r.createdAt!) : '—'; return MCard( onTap: () => _showDetail(r), nm: _planLabel(r.productBizCode), sub: r.outTradeNo, badges: [DsIconBadge(_statusLabel(r.status))], amt: '¥${r.displayAmount}', foot: r.userName.isEmpty ? createdText : '$createdText · ${r.userName}', ); } // ── 详情抽屉/sheet ──────────────────────────────────────────── void _showDetail(PurchaseRecord r) { showOrderDetailDrawer( context, builder: (ctx) => _PurchaseDetailPanel( record: r, onGoInfo: () { Navigator.of(ctx).pop(); widget.onGoInfo(); }, onGoBuy: widget.onGoBuy == null ? null : () { Navigator.of(ctx).pop(); widget.onGoBuy!(); }, onCancel: () => _cancelOrder(ctx, r), onContinuePay: () => _continuePay(r), ), ); } Future _cancelOrder(BuildContext sheetCtx, PurchaseRecord r) async { try { final ok = await ref.read(licenseRepositoryProvider).cancelPurchase(r.outTradeNo); if (!mounted) return; if (ok) { if (sheetCtx.mounted) Navigator.of(sheetCtx).pop(); ref.read(purchaseListProvider.notifier).reload(); showDsToast(context, '订单已取消', bg: context.tokens.success); } else { showDsToast(context, '取消失败,可能已支付,请刷新', bg: context.tokens.danger); } } on AppException catch (_) { if (!mounted) return; showDsToast(context, '取消失败,可能已支付,请刷新', bg: context.tokens.danger); } } Future _continuePay(PurchaseRecord r) async { if (r.payUrl.isEmpty) { if (mounted) showDsToast(context, '支付链接不可用,请刷新后重试'); return; } try { await launchUrl(Uri.parse(r.payUrl), mode: LaunchMode.externalApplication); } catch (e) { if (mounted) { showDsToast(context, '无法打开支付链接,请刷新后重试', bg: context.tokens.danger); } } } } /// 订单详情面板内容(桌面右滑抽屉 / 移动底部 sheet 共用同一 body, /// 由 showOrderDetailDrawer 决定外层壳;本面板自带标题行 + 关闭按钮)。 class _PurchaseDetailPanel extends StatelessWidget { final PurchaseRecord record; final VoidCallback onGoInfo; final VoidCallback? onGoBuy; final Future Function() onCancel; final Future Function() onContinuePay; const _PurchaseDetailPanel({ required this.record, required this.onGoInfo, required this.onGoBuy, required this.onCancel, required this.onContinuePay, }); @override Widget build(BuildContext context) { final t = context.tokens; final r = record; final rows = <(String, Widget)>[ ('订单号', Text(r.outTradeNo, style: const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback))), ('套餐', Text(_planLabel(r.productBizCode))), ('授权时长', Text('${_planDays(r.productBizCode) ?? '—'} 天')), ('金额', Text('¥${r.displayAmount}', style: const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback))), ( '状态', DsBadge(_statusLabel(r.status), tone: _statusTone(r.status), icon: statusIcon(_statusLabel(r.status))) ), ( '支付渠道', Text(r.status == 'paid' ? '支付宝' : '—', style: r.status == 'paid' ? null : TextStyle(color: t.faint)) ), ('下单人', Text(r.userName.isEmpty ? '—' : r.userName)), ( '下单时间', Text( r.createdAt != null ? DateFormat('yyyy-MM-dd HH:mm').format(r.createdAt!) : '—', style: const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: AppDims.fsSm)) ), ( '支付时间', r.paidAt != null ? Text(DateFormat('yyyy-MM-dd HH:mm').format(r.paidAt!), style: const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: AppDims.fsSm)) : Text('—', style: TextStyle(color: t.faint)) ), if (r.status == 'paid') ( '授权续期至', Text( r.renewedTo != null ? DateFormat('yyyy-MM-dd').format(r.renewedTo!) : '—', style: TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontWeight: FontWeight.w600, color: t.success)) ), ]; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.fromLTRB(20, 18, 16, 14), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.border))), child: Row(children: [ Expanded( child: Text('订单详情', style: TextStyle( fontSize: AppDims.fsH2, fontWeight: FontWeight.w700, color: t.heading)), ), IconButton( icon: Icon(LucideIcons.x, size: 20, color: t.muted), splashRadius: 18, onPressed: () => Navigator.of(context).pop(), ), ]), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 14, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final row in rows) _drow(t, row.$1, row.$2), if (r.status == 'pending') ...[ const SizedBox(height: 14), _notice(t, '订单未支付将在下单 2 小时后自动关闭;' '支付成功后授权时长自动叠加到当前到期日,不浪费剩余天数。'), ], if (r.status == 'failed') ...[ const SizedBox(height: 14), _notice(t, '订单已关闭。如需购买请重新下单,价格以下单时套餐表为准。'), ], const SizedBox(height: 18), _actions(context, t), ], ), ), ), ], ); } Widget _drow(dynamic t, String label, Widget value) => Container( padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.borderSubtle))), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)), const Spacer(), DefaultTextStyle( style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w600, color: t.text), child: value, ), ], ), ); Widget _notice(dynamic t, String text) => Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: t.infoSoft, borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(LucideIcons.info, size: 16, color: t.primary), const SizedBox(width: 8), Expanded( child: Text(text, style: TextStyle(fontSize: AppDims.fsSm, color: t.text)), ), ], ), ); Widget _actions(BuildContext context, dynamic t) { final buttons = []; switch (record.status) { case 'pending': buttons.add(WriteGuard( child: DsButton('取消订单', small: true, variant: DsBtnVariant.danger, onPressed: onCancel), )); buttons.add(DsButton('继续支付', small: true, icon: LucideIcons.creditCard, variant: DsBtnVariant.primary, onPressed: onContinuePay)); break; case 'failed': if (onGoBuy != null) { buttons.add(DsButton('重新购买', small: true, icon: LucideIcons.plus, variant: DsBtnVariant.primary, onPressed: onGoBuy)); } break; case 'paid': buttons.add(DsButton('查看授权', small: true, icon: LucideIcons.shieldCheck, onPressed: onGoInfo)); break; } if (buttons.isEmpty) return const SizedBox.shrink(); return Wrap(spacing: 10, runSpacing: 10, children: buttons); } }