7849f6e7e5
- 修复 CardTheme → CardThemeData(Flutter 3.41 API 变更) - 修复 finance_screen 缺少 FormDialog import - 修复工具栏 Row overflow(添加横向滚动) - flutter create --platforms=macos,web 生成平台配置 - 添加 docs/dev-setup.md:完整开发环境配置指南 含 Go/Flutter/Docker 安装、已知 macOS 26 兼容问题及解决方案 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
883 lines
29 KiB
Dart
883 lines
29 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../widgets/page_scaffold.dart';
|
|
import '../../widgets/data_table_card.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../widgets/form_dialog.dart';
|
|
|
|
class FinanceScreen extends ConsumerStatefulWidget {
|
|
const FinanceScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<FinanceScreen> createState() => _FinanceScreenState();
|
|
}
|
|
|
|
class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|
int _page = 1;
|
|
String _monthFilter = '2026-04';
|
|
String _typeFilter = '全部';
|
|
|
|
final List<Map<String, dynamic>> _transactions = [
|
|
{
|
|
'no': 'FN20260401001',
|
|
'type': '应付账款',
|
|
'partner': '贵州茅台酒股份有限公司',
|
|
'orderNo': 'RK20260401001',
|
|
'amount': '85000.00',
|
|
'paid': '85000.00',
|
|
'balance': '0.00',
|
|
'date': '2026-04-01',
|
|
'dueDate': '2026-05-01',
|
|
'status': '已结清',
|
|
},
|
|
{
|
|
'no': 'FN20260401002',
|
|
'type': '应付账款',
|
|
'partner': '四川五粮液股份有限公司',
|
|
'orderNo': 'RK20260401002',
|
|
'amount': '42500.00',
|
|
'paid': '0.00',
|
|
'balance': '42500.00',
|
|
'date': '2026-04-01',
|
|
'dueDate': '2026-05-01',
|
|
'status': '未付款',
|
|
},
|
|
{
|
|
'no': 'FN20260402001',
|
|
'type': '应付账款',
|
|
'partner': '江苏洋河酒厂股份有限公司',
|
|
'orderNo': 'RK20260402001',
|
|
'amount': '36800.00',
|
|
'paid': '20000.00',
|
|
'balance': '16800.00',
|
|
'date': '2026-04-02',
|
|
'dueDate': '2026-05-17',
|
|
'status': '部分付款',
|
|
},
|
|
{
|
|
'no': 'FN20260402002',
|
|
'type': '应付账款',
|
|
'partner': '泸州老窖股份有限公司',
|
|
'orderNo': 'RK20260403001',
|
|
'amount': '55200.00',
|
|
'paid': '55200.00',
|
|
'balance': '0.00',
|
|
'date': '2026-04-03',
|
|
'dueDate': '2026-05-03',
|
|
'status': '已结清',
|
|
},
|
|
{
|
|
'no': 'FN20260403001',
|
|
'type': '应付账款',
|
|
'partner': '法国拉菲集团中国总代理',
|
|
'orderNo': 'RK20260403002',
|
|
'amount': '124800.00',
|
|
'paid': '0.00',
|
|
'balance': '124800.00',
|
|
'date': '2026-04-03',
|
|
'dueDate': '2026-06-02',
|
|
'status': '未付款',
|
|
},
|
|
{
|
|
'no': 'FN20260404001',
|
|
'type': '应付账款',
|
|
'partner': '人头马轩尼诗(中国)有限公司',
|
|
'orderNo': 'RK20260404001',
|
|
'amount': '30240.00',
|
|
'paid': '0.00',
|
|
'balance': '30240.00',
|
|
'date': '2026-04-04',
|
|
'dueDate': '2026-06-03',
|
|
'status': '未付款',
|
|
},
|
|
{
|
|
'no': 'FN20260404002',
|
|
'type': '费用报销',
|
|
'partner': '张三',
|
|
'orderNo': 'BX20260404001',
|
|
'amount': '1280.00',
|
|
'paid': '1280.00',
|
|
'balance': '0.00',
|
|
'date': '2026-04-04',
|
|
'dueDate': '2026-04-04',
|
|
'status': '已结清',
|
|
},
|
|
];
|
|
|
|
// Summary stats
|
|
double get _totalPayable => _transactions
|
|
.where((t) => t['type'] == '应付账款')
|
|
.fold(0.0, (s, t) => s + (double.tryParse(t['balance'] as String) ?? 0));
|
|
|
|
double get _totalPaid => _transactions.fold(
|
|
0.0, (s, t) => s + (double.tryParse(t['paid'] as String) ?? 0));
|
|
|
|
double get _totalAmount => _transactions.fold(
|
|
0.0, (s, t) => s + (double.tryParse(t['amount'] as String) ?? 0));
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PageScaffold(
|
|
title: '财务管理',
|
|
tabs: const [
|
|
Tab(text: '应付账款'),
|
|
Tab(text: '付款记录'),
|
|
Tab(text: '财务报表'),
|
|
],
|
|
tabViews: [
|
|
_buildPayableList(),
|
|
_buildPaymentHistory(),
|
|
_buildFinanceReport(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPayableList() {
|
|
final filtered = _typeFilter == '全部'
|
|
? _transactions
|
|
: _transactions.where((t) => t['type'] == _typeFilter).toList();
|
|
|
|
return Column(
|
|
children: [
|
|
// Summary bar
|
|
Container(
|
|
color: AppTheme.background,
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
children: [
|
|
_FinanceSummaryCard(
|
|
title: '本月采购总额',
|
|
value: '¥${(_totalAmount / 10000).toStringAsFixed(1)}万',
|
|
icon: Icons.shopping_bag,
|
|
color: AppTheme.primary,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_FinanceSummaryCard(
|
|
title: '已付款',
|
|
value: '¥${(_totalPaid / 10000).toStringAsFixed(1)}万',
|
|
icon: Icons.check_circle,
|
|
color: AppTheme.success,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_FinanceSummaryCard(
|
|
title: '未付款',
|
|
value: '¥${(_totalPayable / 10000).toStringAsFixed(1)}万',
|
|
icon: Icons.pending_actions,
|
|
color: AppTheme.danger,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_FinanceSummaryCard(
|
|
title: '即将到期(7天)',
|
|
value: '¥16.8万',
|
|
icon: Icons.alarm,
|
|
color: AppTheme.accent,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: DataTableCard(
|
|
totalCount: filtered.length,
|
|
page: _page,
|
|
onPageChanged: (p) => setState(() => _page = p),
|
|
toolbar: Row(
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () => _showPaymentDialog(context),
|
|
icon: const Icon(Icons.payment, size: 16),
|
|
label: const Text('登记付款'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.file_download_outlined, size: 16),
|
|
label: const Text('导出对账单'),
|
|
),
|
|
const Spacer(),
|
|
// Month filter
|
|
_MonthSelector(
|
|
value: _monthFilter,
|
|
onChanged: (v) => setState(() => _monthFilter = v),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_DropdownFilter(
|
|
value: _typeFilter,
|
|
items: ['全部', '应付账款', '费用报销'],
|
|
onChanged: (v) => setState(() => _typeFilter = v!),
|
|
hint: '类型',
|
|
),
|
|
],
|
|
),
|
|
columns: const [
|
|
DataColumn(label: Text('凭证号')),
|
|
DataColumn(label: Text('类型')),
|
|
DataColumn(label: Text('往来单位')),
|
|
DataColumn(label: Text('关联单号')),
|
|
DataColumn(label: Text('应付金额'), numeric: true),
|
|
DataColumn(label: Text('已付金额'), numeric: true),
|
|
DataColumn(label: Text('余额'), numeric: true),
|
|
DataColumn(label: Text('到期日')),
|
|
DataColumn(label: Text('状态')),
|
|
DataColumn(label: Text('操作')),
|
|
],
|
|
rows: filtered
|
|
.map((t) => DataRow(
|
|
color: WidgetStateProperty.resolveWith((states) {
|
|
if (t['status'] == '未付款') {
|
|
final due = DateTime.tryParse(t['dueDate'] as String);
|
|
if (due != null &&
|
|
due.difference(DateTime.now()).inDays <= 7) {
|
|
return AppTheme.accent.withOpacity(0.05);
|
|
}
|
|
}
|
|
return null;
|
|
}),
|
|
cells: [
|
|
DataCell(Text(t['no'] as String,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
color: AppTheme.textSecondary))),
|
|
DataCell(Text(t['type'] as String)),
|
|
DataCell(SizedBox(
|
|
width: 160,
|
|
child: Text(t['partner'] as String,
|
|
overflow: TextOverflow.ellipsis),
|
|
)),
|
|
DataCell(Text(t['orderNo'] as String,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
color: AppTheme.primary))),
|
|
DataCell(Text('¥${t['amount']}',
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w500))),
|
|
DataCell(Text('¥${t['paid']}',
|
|
style: const TextStyle(
|
|
color: AppTheme.success))),
|
|
DataCell(Text(
|
|
'¥${t['balance']}',
|
|
style: TextStyle(
|
|
color: (t['balance'] as String) != '0.00'
|
|
? AppTheme.danger
|
|
: AppTheme.textSecondary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
)),
|
|
DataCell(Text(t['dueDate'] as String)),
|
|
DataCell(_PaymentStatusBadge(t['status'] as String)),
|
|
DataCell(Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () {},
|
|
child: const Text('详情',
|
|
style: TextStyle(fontSize: 12))),
|
|
if (t['status'] != '已结清')
|
|
TextButton(
|
|
onPressed: () =>
|
|
_showPaymentDialog(context),
|
|
child: const Text('付款',
|
|
style: TextStyle(
|
|
color: AppTheme.success,
|
|
fontSize: 12))),
|
|
],
|
|
)),
|
|
],
|
|
))
|
|
.toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPaymentHistory() {
|
|
final payments = [
|
|
{
|
|
'date': '2026-04-01',
|
|
'no': 'ZF20260401001',
|
|
'partner': '贵州茅台酒股份有限公司',
|
|
'amount': '85000.00',
|
|
'method': '银行转账',
|
|
'bank': '招商银行',
|
|
'remark': '4月货款结清',
|
|
},
|
|
{
|
|
'date': '2026-04-02',
|
|
'no': 'ZF20260402001',
|
|
'partner': '泸州老窖股份有限公司',
|
|
'amount': '55200.00',
|
|
'method': '银行转账',
|
|
'bank': '工商银行',
|
|
'remark': '4月货款',
|
|
},
|
|
{
|
|
'date': '2026-04-02',
|
|
'no': 'ZF20260402002',
|
|
'partner': '江苏洋河酒厂股份有限公司',
|
|
'amount': '20000.00',
|
|
'method': '银行转账',
|
|
'bank': '建设银行',
|
|
'remark': '部分预付',
|
|
},
|
|
{
|
|
'date': '2026-04-04',
|
|
'no': 'ZF20260404001',
|
|
'partner': '张三',
|
|
'amount': '1280.00',
|
|
'method': '现金',
|
|
'bank': '-',
|
|
'remark': '差旅报销',
|
|
},
|
|
];
|
|
|
|
return DataTableCard(
|
|
totalCount: payments.length,
|
|
page: 1,
|
|
toolbar: Row(
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () => _showPaymentDialog(context),
|
|
icon: const Icon(Icons.add, size: 16),
|
|
label: const Text('新增付款'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.file_download_outlined, size: 16),
|
|
label: const Text('导出'),
|
|
),
|
|
const Spacer(),
|
|
_MonthSelector(
|
|
value: _monthFilter,
|
|
onChanged: (v) => setState(() => _monthFilter = v),
|
|
),
|
|
],
|
|
),
|
|
columns: const [
|
|
DataColumn(label: Text('付款日期')),
|
|
DataColumn(label: Text('付款单号')),
|
|
DataColumn(label: Text('收款方')),
|
|
DataColumn(label: Text('金额'), numeric: true),
|
|
DataColumn(label: Text('付款方式')),
|
|
DataColumn(label: Text('银行')),
|
|
DataColumn(label: Text('备注')),
|
|
DataColumn(label: Text('操作')),
|
|
],
|
|
rows: payments
|
|
.map((p) => DataRow(cells: [
|
|
DataCell(Text(p['date'] as String)),
|
|
DataCell(Text(p['no'] as String,
|
|
style: const TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 11,
|
|
color: AppTheme.textSecondary))),
|
|
DataCell(SizedBox(
|
|
width: 160,
|
|
child: Text(p['partner'] as String,
|
|
overflow: TextOverflow.ellipsis),
|
|
)),
|
|
DataCell(Text(
|
|
'¥${p['amount']}',
|
|
style: const TextStyle(
|
|
color: AppTheme.danger, fontWeight: FontWeight.w600),
|
|
)),
|
|
DataCell(Text(p['method'] as String)),
|
|
DataCell(Text(p['bank'] as String)),
|
|
DataCell(Text(p['remark'] as String)),
|
|
DataCell(TextButton(
|
|
onPressed: () {},
|
|
child: const Text('查看',
|
|
style: TextStyle(fontSize: 12)))),
|
|
]))
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
Widget _buildFinanceReport() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Month/period selector
|
|
Row(
|
|
children: [
|
|
const Text('报表周期:',
|
|
style: TextStyle(fontSize: 14)),
|
|
const SizedBox(width: 8),
|
|
_MonthSelector(
|
|
value: _monthFilter,
|
|
onChanged: (v) => setState(() => _monthFilter = v),
|
|
),
|
|
const SizedBox(width: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.picture_as_pdf, size: 16),
|
|
label: const Text('导出报表'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Metrics row
|
|
Row(
|
|
children: [
|
|
_ReportCard(
|
|
title: '本月采购总额',
|
|
value: '¥${(_totalAmount / 10000).toStringAsFixed(2)}万',
|
|
change: '+12.5%',
|
|
positive: false,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_ReportCard(
|
|
title: '本月付款总额',
|
|
value: '¥${(_totalPaid / 10000).toStringAsFixed(2)}万',
|
|
change: '+8.3%',
|
|
positive: true,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_ReportCard(
|
|
title: '应付账款余额',
|
|
value: '¥${(_totalPayable / 10000).toStringAsFixed(2)}万',
|
|
change: '-5.2%',
|
|
positive: true,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_ReportCard(
|
|
title: '库存总价值',
|
|
value: '¥103.17万',
|
|
change: '+3.8%',
|
|
positive: false,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Category breakdown
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('按品类采购金额',
|
|
style: TextStyle(
|
|
fontSize: 14, fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 16),
|
|
...[
|
|
('白酒', 238600.0, AppTheme.primary),
|
|
('葡萄酒', 124800.0, const Color(0xFF7B1FA2)),
|
|
('洋酒', 30240.0, AppTheme.accent),
|
|
('啤酒', 1970.0, const Color(0xFF0097A7)),
|
|
].map((item) {
|
|
final total = 395610.0;
|
|
final pct = item.$2 / total;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 60,
|
|
child: Text(item.$1,
|
|
style: const TextStyle(fontSize: 13))),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: LinearProgressIndicator(
|
|
value: pct,
|
|
backgroundColor: AppTheme.border,
|
|
color: item.$3,
|
|
minHeight: 8,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
SizedBox(
|
|
width: 100,
|
|
child: Text(
|
|
'¥${(item.$2 / 10000).toStringAsFixed(1)}万 (${(pct * 100).toStringAsFixed(1)}%)',
|
|
style: const TextStyle(fontSize: 12),
|
|
textAlign: TextAlign.right,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showPaymentDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => FormDialog(
|
|
title: '登记付款',
|
|
width: 480,
|
|
onConfirm: () {
|
|
Navigator.of(ctx).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('付款记录已登记'),
|
|
backgroundColor: AppTheme.success,
|
|
),
|
|
);
|
|
},
|
|
content: Column(
|
|
children: [
|
|
_DialogRow(
|
|
children: [
|
|
_DialogField(
|
|
label: '付款方式',
|
|
child: DropdownButtonFormField<String>(
|
|
value: '银行转账',
|
|
items: ['银行转账', '现金', '支票', '网银']
|
|
.map((s) => DropdownMenuItem(
|
|
value: s,
|
|
child: Text(s,
|
|
style: const TextStyle(fontSize: 13))))
|
|
.toList(),
|
|
onChanged: (_) {},
|
|
decoration: const InputDecoration(),
|
|
)),
|
|
_DialogField(
|
|
label: '付款金额',
|
|
required: true,
|
|
child: TextFormField(
|
|
decoration: const InputDecoration(
|
|
hintText: '0.00', prefixText: '¥'),
|
|
keyboardType:
|
|
const TextInputType.numberWithOptions(decimal: true))),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_DialogRow(
|
|
children: [
|
|
_DialogField(
|
|
label: '付款银行',
|
|
child: DropdownButtonFormField<String>(
|
|
value: '招商银行',
|
|
items: ['招商银行', '工商银行', '建设银行', '农业银行', '中国银行']
|
|
.map((s) => DropdownMenuItem(
|
|
value: s,
|
|
child: Text(s,
|
|
style: const TextStyle(fontSize: 13))))
|
|
.toList(),
|
|
onChanged: (_) {},
|
|
decoration: const InputDecoration(),
|
|
)),
|
|
_DialogField(
|
|
label: '付款日期',
|
|
child: InputDecorator(
|
|
decoration: const InputDecoration(),
|
|
child: const Text('2026-04-04',
|
|
style: TextStyle(fontSize: 13)),
|
|
)),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_DialogField(
|
|
label: '备注',
|
|
fullWidth: true,
|
|
child: TextFormField(
|
|
maxLines: 2,
|
|
decoration: const InputDecoration(hintText: '付款说明'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DialogRow extends StatelessWidget {
|
|
final List<Widget> children;
|
|
const _DialogRow({required this.children});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: children
|
|
.expand((child) => [
|
|
Expanded(child: child),
|
|
if (child != children.last) const SizedBox(width: 12),
|
|
])
|
|
.toList(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DialogField extends StatelessWidget {
|
|
final String label;
|
|
final Widget child;
|
|
final bool required;
|
|
final bool fullWidth;
|
|
|
|
const _DialogField({
|
|
required this.label,
|
|
required this.child,
|
|
this.required = false,
|
|
this.fullWidth = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget content = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(children: [
|
|
if (required)
|
|
const Text('* ',
|
|
style: TextStyle(color: AppTheme.danger, fontSize: 13)),
|
|
Text(label,
|
|
style: const TextStyle(
|
|
fontSize: 13, color: AppTheme.textSecondary)),
|
|
]),
|
|
const SizedBox(height: 6),
|
|
child,
|
|
],
|
|
);
|
|
return fullWidth ? SizedBox(width: double.infinity, child: content) : content;
|
|
}
|
|
}
|
|
|
|
class _FinanceSummaryCard extends StatelessWidget {
|
|
final String title;
|
|
final String value;
|
|
final IconData icon;
|
|
final Color color;
|
|
|
|
const _FinanceSummaryCard({
|
|
required this.title,
|
|
required this.value,
|
|
required this.icon,
|
|
required this.color,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Expanded(
|
|
child: Container(
|
|
height: 72,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.surface,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: AppTheme.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: const TextStyle(
|
|
fontSize: 12, color: AppTheme.textSecondary)),
|
|
const SizedBox(height: 4),
|
|
Text(value,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: color)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PaymentStatusBadge extends StatelessWidget {
|
|
final String status;
|
|
const _PaymentStatusBadge(this.status);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Color bg, fg;
|
|
switch (status) {
|
|
case '已结清':
|
|
bg = const Color(0xFFE8F5E9);
|
|
fg = AppTheme.success;
|
|
break;
|
|
case '部分付款':
|
|
bg = const Color(0xFFFFF3E0);
|
|
fg = AppTheme.accent;
|
|
break;
|
|
case '未付款':
|
|
bg = const Color(0xFFFFEBEE);
|
|
fg = AppTheme.danger;
|
|
break;
|
|
default:
|
|
bg = const Color(0xFFF5F5F5);
|
|
fg = AppTheme.textSecondary;
|
|
}
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration:
|
|
BoxDecoration(color: bg, borderRadius: BorderRadius.circular(3)),
|
|
child: Text(status,
|
|
style: TextStyle(
|
|
color: fg, fontSize: 12, fontWeight: FontWeight.w500)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ReportCard extends StatelessWidget {
|
|
final String title;
|
|
final String value;
|
|
final String change;
|
|
final bool positive;
|
|
|
|
const _ReportCard({
|
|
required this.title,
|
|
required this.value,
|
|
required this.change,
|
|
required this.positive,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Expanded(
|
|
child: Container(
|
|
height: 88,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.surface,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: AppTheme.border, width: 0.5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title,
|
|
style: const TextStyle(
|
|
fontSize: 12, color: AppTheme.textSecondary)),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(value,
|
|
style: const TextStyle(
|
|
fontSize: 18, fontWeight: FontWeight.w700)),
|
|
const Spacer(),
|
|
Text(
|
|
change,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: positive ? AppTheme.success : AppTheme.danger,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MonthSelector extends StatelessWidget {
|
|
final String value;
|
|
final ValueChanged<String> onChanged;
|
|
|
|
const _MonthSelector({required this.value, required this.onChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final months = [
|
|
'2026-04',
|
|
'2026-03',
|
|
'2026-02',
|
|
'2026-01',
|
|
'2025-12',
|
|
'2025-11',
|
|
];
|
|
return Container(
|
|
height: 36,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: AppTheme.border),
|
|
borderRadius: BorderRadius.circular(4),
|
|
color: AppTheme.surface,
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
items: months
|
|
.map((m) => DropdownMenuItem(
|
|
value: m,
|
|
child: Text(m, style: const TextStyle(fontSize: 13))))
|
|
.toList(),
|
|
onChanged: (v) => onChanged(v!),
|
|
style: const TextStyle(
|
|
fontSize: 13, color: AppTheme.textPrimary),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DropdownFilter extends StatelessWidget {
|
|
final String value;
|
|
final List<String> items;
|
|
final ValueChanged<String?> onChanged;
|
|
final String hint;
|
|
|
|
const _DropdownFilter({
|
|
required this.value,
|
|
required this.items,
|
|
required this.onChanged,
|
|
required this.hint,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: 36,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: AppTheme.border),
|
|
borderRadius: BorderRadius.circular(4),
|
|
color: AppTheme.surface,
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
value: value,
|
|
items: items
|
|
.map((s) => DropdownMenuItem(
|
|
value: s,
|
|
child: Text(s, style: const TextStyle(fontSize: 13))))
|
|
.toList(),
|
|
onChanged: onChanged,
|
|
style: const TextStyle(
|
|
fontSize: 13, color: AppTheme.textPrimary),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|