diff --git a/client/lib/screens/finance/finance_screen.dart b/client/lib/screens/finance/finance_screen.dart index 81067e2..12359b8 100644 --- a/client/lib/screens/finance/finance_screen.dart +++ b/client/lib/screens/finance/finance_screen.dart @@ -3,9 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/responsive/responsive.dart'; import '../../core/config/app_constants.dart'; import '../../core/theme/context_tokens.dart'; +import '../../core/theme/app_dims.g.dart'; import '../../models/finance.dart'; import '../../providers/finance_provider.dart'; import '../../widgets/data_table_card.dart'; +import '../../widgets/kpi_card.dart'; import '../../widgets/mobile_list_card.dart'; import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader; @@ -374,41 +376,46 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> { children: [ if (widget.typeFilter.isNotEmpty && records.isNotEmpty) Builder(builder: (context) { - final w = context.isMobile ? 150.0 : null; - final cards = [ - _SummaryCard( + final mobile = context.isMobile; + final cards = [ + KpiCard( title: '合计金额', value: '¥${(totalAmount / 10000).toStringAsFixed(2)}万', icon: Icons.account_balance_wallet, - color: context.tokens.primary, - width: w, + tone: KpiTone.info, ), - const SizedBox(width: 12), - _SummaryCard( + KpiCard( title: '未结清', value: '¥${(openAmount / 10000).toStringAsFixed(2)}万', icon: Icons.pending_actions, - color: context.tokens.danger, - width: w, + tone: KpiTone.warn, ), - const SizedBox(width: 12), - _SummaryCard( + KpiCard( title: '已结清', value: '¥${(closedAmount / 10000).toStringAsFixed(2)}万', icon: Icons.check_circle, - color: context.tokens.success, - width: w, + tone: KpiTone.ok, ), ]; + final row = []; + for (var i = 0; i < cards.length; i++) { + if (i > 0) row.add(const SizedBox(width: AppDims.sp3)); + row.add(mobile + ? SizedBox(width: 160, child: cards[i]) + : Expanded(child: cards[i])); + } return Container( color: context.tokens.bg, - padding: const EdgeInsets.all(12), - child: context.isMobile + padding: const EdgeInsets.all(AppDims.sp3), + child: mobile ? SingleChildScrollView( scrollDirection: Axis.horizontal, - child: Row(children: cards), + child: IntrinsicHeight(child: Row(children: row)), ) - : Row(children: cards), + : IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: row)), ); }), if (widget.typeFilter.isNotEmpty && records.isNotEmpty) @@ -647,22 +654,10 @@ class _StatusBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final isOpen = status == 'open'; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: isOpen ? const Color(0xFFFFF3E0) : const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(3), - ), - child: Text( - isOpen ? '未结清' : '已结清', - style: TextStyle( - color: isOpen ? context.tokens.accent : context.tokens.muted, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ); + final t = context.tokens; + return status == 'open' + ? StatusPill(label: '未结清', color: t.warn, background: t.warnBg) + : StatusPill(label: '已结清', color: t.muted, background: t.infoSoft); } } @@ -672,95 +667,15 @@ class _TypeBadge extends StatelessWidget { @override Widget build(BuildContext context) { - Color bg, fg; - switch (label) { - case '应收账款': - bg = const Color(0xFFE3F2FD); - fg = context.tokens.primary; - break; - case '应付账款': - bg = const Color(0xFFFFEBEE); - fg = context.tokens.danger; - break; - case '收款': - bg = const Color(0xFFE8F5E9); - fg = context.tokens.success; - break; - case '付款': - bg = const Color(0xFFFFF3E0); - fg = context.tokens.accent; - break; - default: - bg = const Color(0xFFF5F5F5); - fg = context.tokens.muted; - } - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: - BoxDecoration(color: bg, borderRadius: BorderRadius.circular(3)), - child: Text(label, - style: - TextStyle(color: fg, fontSize: 12, fontWeight: FontWeight.w500)), - ); - } -} - -class _SummaryCard extends StatelessWidget { - final String title; - final String value; - final IconData icon; - final Color color; - final double? width; - - const _SummaryCard({ - required this.title, - required this.value, - required this.icon, - required this.color, - this.width, - }); - - @override - Widget build(BuildContext context) { - final card = Container( - height: 72, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: context.tokens.surface, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: context.tokens.border, width: 0.5), - ), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon(icon, color: color, size: 22), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(title, - style: TextStyle( - fontSize: 12, color: context.tokens.muted)), - const SizedBox(height: 4), - Text(value, - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.w700, color: color)), - ], - ), - ], - ), - ); - return width != null - ? SizedBox(width: width, child: card) - : Expanded(child: card); + final t = context.tokens; + final (Color fg, Color bg) = switch (label) { + '应收账款' => (t.primary, t.infoSoft), + '应付账款' => (t.danger, t.dangerBg), + '收款' => (t.success, t.okSoft), + '付款' => (t.warn, t.warnBg), + _ => (t.muted, t.infoSoft), + }; + return StatusPill(label: label, color: fg, background: bg); } } @@ -809,22 +724,23 @@ class _OfflineBanner extends StatelessWidget { @override Widget build(BuildContext context) { + final t = context.tokens; return Container( width: double.infinity, - color: const Color(0xFFFFF8E1), + color: t.warnBg, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ - const Icon(Icons.cloud_off, size: 14, color: Color(0xFFF57F17)), + Icon(Icons.cloud_off, size: 14, color: t.warn), const SizedBox(width: 8), - const Expanded( + Expanded( child: Text('网络不可用,当前显示离线缓存数据', - style: TextStyle(color: Color(0xFFF57F17), fontSize: 12)), + style: TextStyle(color: t.warn, fontSize: 12)), ), TextButton( onPressed: onRetry, style: TextButton.styleFrom( - foregroundColor: const Color(0xFFF57F17), + foregroundColor: t.warn, padding: const EdgeInsets.symmetric(horizontal: 8)), child: const Text('重试', style: TextStyle(fontSize: 12)), ), diff --git a/client/test/golden/finance_golden_test.dart b/client/test/golden/finance_golden_test.dart new file mode 100644 index 0000000..049a8d1 --- /dev/null +++ b/client/test/golden/finance_golden_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:jiu_client/core/models/page_result.dart'; +import 'package:jiu_client/core/auth/auth_state.dart'; +import 'package:jiu_client/models/finance.dart'; +import 'package:jiu_client/providers/finance_provider.dart'; +import 'package:jiu_client/repositories/finance_repository.dart'; +import 'package:jiu_client/screens/finance/finance_screen.dart'; + +import '../support/golden_harness.dart'; + +/// design-distill 阶段4:财务整屏 golden × 三主题(桌面 + 移动)。 +/// 验证类型徽章(应收/应付/收款/付款)+ 结清状态徽章 → StatusPill 随主题换色。 +/// 更新基准:flutter test --update-goldens test/golden/finance_golden_test.dart + +const _records = [ + FinanceRecord( + id: 1, type: 'receivable', amount: 126800, balance: 126800, + status: 'open', partnerName: '鼎丰超市', recordDate: '2026-06-20'), + FinanceRecord( + id: 2, type: 'payable', amount: 152600, balance: 0, + status: 'closed', partnerName: '老窖酒水批发', recordDate: '2026-06-18'), + FinanceRecord( + id: 3, type: 'receipt', amount: 54200, balance: 0, + status: 'closed', partnerName: '金樽酒楼', recordDate: '2026-06-15'), + FinanceRecord( + id: 4, type: 'payment', amount: 88400, balance: 88400, + status: 'open', partnerName: '汾酒山西直营', recordDate: '2026-06-12'), +]; + +// 财务屏经 FutureBuilder 直读 repository,故覆盖 repo(implements + noSuchMethod +// 只实现 listRecords,其余方法 golden 静态渲染不会触达)。 +class _FakeFinanceRepo implements FinanceRepository { + @override + Future> listRecords({ + String? type, + String? month, + int page = 1, + int pageSize = 50, + }) async => + const PageResult(data: _records, total: 4, page: 1, pageSize: 20); + + @override + dynamic noSuchMethod(Invocation invocation) => + super.noSuchMethod(invocation); +} + +List _overrides() => [ + financeRepositoryProvider.overrideWithValue(_FakeFinanceRepo()), + isReadonlyProvider.overrideWithValue(false), + ]; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + + goldenAcrossThemes( + 'finance 整屏(桌面)', + goldenPrefix: 'finance', + child: () => const Scaffold(body: FinanceScreen()), + overrides: _overrides, + logical: const Size(1280, 900), + ); + + goldenAcrossThemes( + 'finance 整屏(移动)', + goldenPrefix: 'finance_mobile', + child: () => const Scaffold(body: FinanceScreen()), + overrides: _overrides, + logical: const Size(390, 1300), + ); +} diff --git a/client/test/golden/goldens/finance_a.png b/client/test/golden/goldens/finance_a.png new file mode 100644 index 0000000..e06c982 Binary files /dev/null and b/client/test/golden/goldens/finance_a.png differ diff --git a/client/test/golden/goldens/finance_b.png b/client/test/golden/goldens/finance_b.png new file mode 100644 index 0000000..8f9c19b Binary files /dev/null and b/client/test/golden/goldens/finance_b.png differ diff --git a/client/test/golden/goldens/finance_c.png b/client/test/golden/goldens/finance_c.png new file mode 100644 index 0000000..f45f24a Binary files /dev/null and b/client/test/golden/goldens/finance_c.png differ diff --git a/client/test/golden/goldens/finance_mobile_a.png b/client/test/golden/goldens/finance_mobile_a.png new file mode 100644 index 0000000..d85bbf0 Binary files /dev/null and b/client/test/golden/goldens/finance_mobile_a.png differ diff --git a/client/test/golden/goldens/finance_mobile_b.png b/client/test/golden/goldens/finance_mobile_b.png new file mode 100644 index 0000000..b4bf597 Binary files /dev/null and b/client/test/golden/goldens/finance_mobile_b.png differ diff --git a/client/test/golden/goldens/finance_mobile_c.png b/client/test/golden/goldens/finance_mobile_c.png new file mode 100644 index 0000000..2845752 Binary files /dev/null and b/client/test/golden/goldens/finance_mobile_c.png differ