feat(client): 全模块 API 对接 + 修复登陆跳转 + 菜单 UI 优化
API 对接: - 入库/出库/库存/财务/往来单位/基础数据全部对接后端 REST API - 新增 repositories、providers、models 层,统一分层架构 - auth 从 flutter_secure_storage 迁移到 shared_preferences 登陆跳转修复: - 将 _RouterNotifier 提取为独立 Riverpod provider,appRouterProvider 使用 ref.read 避免依赖链导致 router 重建后跳回 /login - redirect 函数新增 initialized 守卫,防止 auth 未恢复时误重定向 - 添加调试日志(Router/Auth/ApiClient)定位 401 触发的 logout 链路 退出菜单 UI: - 去掉 ListTile,改用 Row + 自定义 padding,文字左对齐 - MouseRegion + AnimatedContainer 实现 hover 高亮(普通项蓝底/退出红底) - 菜单圆角 6px,elevation 8,分割线高度 1px Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
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/models/page_result.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../widgets/form_dialog.dart';
|
||||
import '../../models/partner.dart';
|
||||
import '../../providers/partner_provider.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
|
||||
class PartnersScreen extends ConsumerStatefulWidget {
|
||||
const PartnersScreen({super.key});
|
||||
@@ -13,147 +16,17 @@ class PartnersScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
int _suppliersPage = 1;
|
||||
int _customersPage = 1;
|
||||
final _searchCtrl = TextEditingController();
|
||||
|
||||
final List<Map<String, dynamic>> _suppliers = [
|
||||
{
|
||||
'code': 'GYS001',
|
||||
'name': '贵州茅台酒股份有限公司',
|
||||
'type': '白酒厂商',
|
||||
'contact': '王建国',
|
||||
'phone': '0851-22886688',
|
||||
'address': '贵州省仁怀市茅台镇',
|
||||
'balance': '-86000.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 30,
|
||||
},
|
||||
{
|
||||
'code': 'GYS002',
|
||||
'name': '四川五粮液股份有限公司',
|
||||
'type': '白酒厂商',
|
||||
'contact': '李明华',
|
||||
'phone': '0831-12345678',
|
||||
'address': '四川省宜宾市翠屏区',
|
||||
'balance': '-42500.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 30,
|
||||
},
|
||||
{
|
||||
'code': 'GYS003',
|
||||
'name': '江苏洋河酒厂股份有限公司',
|
||||
'type': '白酒厂商',
|
||||
'contact': '张秀英',
|
||||
'phone': '0527-83999999',
|
||||
'address': '江苏省宿迁市宿城区',
|
||||
'balance': '0.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 45,
|
||||
},
|
||||
{
|
||||
'code': 'GYS004',
|
||||
'name': '剑南春(集团)有限责任公司',
|
||||
'type': '白酒厂商',
|
||||
'contact': '陈志强',
|
||||
'phone': '0838-88888888',
|
||||
'address': '四川省德阳市绵竹市',
|
||||
'balance': '-28600.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 30,
|
||||
},
|
||||
{
|
||||
'code': 'GYS005',
|
||||
'name': '泸州老窖股份有限公司',
|
||||
'type': '白酒厂商',
|
||||
'contact': '赵丽红',
|
||||
'phone': '0830-12233456',
|
||||
'address': '四川省泸州市江阳区',
|
||||
'balance': '-15200.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 30,
|
||||
},
|
||||
{
|
||||
'code': 'GYS006',
|
||||
'name': '法国拉菲集团中国总代理',
|
||||
'type': '葡萄酒进口商',
|
||||
'contact': 'Pierre Liu',
|
||||
'phone': '021-61234567',
|
||||
'address': '上海市黄浦区外滩18号',
|
||||
'balance': '-124800.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 60,
|
||||
},
|
||||
{
|
||||
'code': 'GYS007',
|
||||
'name': '人头马轩尼诗(中国)有限公司',
|
||||
'type': '洋酒进口商',
|
||||
'contact': '刘经理',
|
||||
'phone': '021-54321678',
|
||||
'address': '上海市浦东新区',
|
||||
'balance': '-30240.00',
|
||||
'status': '合作中',
|
||||
'creditDays': 60,
|
||||
},
|
||||
{
|
||||
'code': 'GYS008',
|
||||
'name': '山西汾酒集团有限责任公司',
|
||||
'type': '白酒厂商',
|
||||
'contact': '周建明',
|
||||
'phone': '0357-33666888',
|
||||
'address': '山西省吕梁市汾阳市',
|
||||
'balance': '0.00',
|
||||
'status': '暂停合作',
|
||||
'creditDays': 30,
|
||||
},
|
||||
];
|
||||
|
||||
final List<Map<String, dynamic>> _customers = [
|
||||
{
|
||||
'code': 'KH001',
|
||||
'name': '北京国贸大酒店',
|
||||
'type': '五星级酒店',
|
||||
'contact': '采购部',
|
||||
'phone': '010-65051234',
|
||||
'address': '北京市朝阳区建国门外大街1号',
|
||||
'balance': '45000.00',
|
||||
'status': '合作中',
|
||||
},
|
||||
{
|
||||
'code': 'KH002',
|
||||
'name': '上海外滩华尔道夫酒店',
|
||||
'type': '五星级酒店',
|
||||
'contact': '采购经理',
|
||||
'phone': '021-63228888',
|
||||
'address': '上海市黄浦区中山东一路2号',
|
||||
'balance': '0.00',
|
||||
'status': '合作中',
|
||||
},
|
||||
{
|
||||
'code': 'KH003',
|
||||
'name': '广州白云国际会议中心',
|
||||
'type': '会议中心',
|
||||
'contact': '餐饮总监',
|
||||
'phone': '020-86001234',
|
||||
'address': '广州市白云区云城东路1号',
|
||||
'balance': '28600.00',
|
||||
'status': '合作中',
|
||||
},
|
||||
{
|
||||
'code': 'KH004',
|
||||
'name': '深圳湾万象城购物中心',
|
||||
'type': '商超零售',
|
||||
'contact': '王采购',
|
||||
'phone': '0755-82345678',
|
||||
'address': '深圳市南山区望海路购物公园',
|
||||
'balance': '12000.00',
|
||||
'status': '合作中',
|
||||
},
|
||||
];
|
||||
final _supplierSearchCtrl = TextEditingController();
|
||||
final _customerSearchCtrl = TextEditingController();
|
||||
Timer? _supplierDebounce;
|
||||
Timer? _customerDebounce;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
_supplierSearchCtrl.dispose();
|
||||
_customerSearchCtrl.dispose();
|
||||
_supplierDebounce?.cancel();
|
||||
_customerDebounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -166,352 +39,436 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
Tab(text: '客户'),
|
||||
],
|
||||
tabViews: [
|
||||
_buildSupplierList(),
|
||||
_buildCustomerList(),
|
||||
_buildSupplierTab(),
|
||||
_buildCustomerTab(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSupplierList() {
|
||||
final q = _searchCtrl.text.toLowerCase();
|
||||
final filtered = _suppliers.where((s) {
|
||||
if (q.isEmpty) return true;
|
||||
return (s['name'] as String).toLowerCase().contains(q) ||
|
||||
(s['code'] as String).toLowerCase().contains(q);
|
||||
}).toList();
|
||||
Widget _buildSupplierTab() {
|
||||
final asyncPartners = ref.watch(supplierListProvider);
|
||||
return asyncPartners.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('加载失败:$e',
|
||||
style: const TextStyle(color: AppTheme.danger)),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(supplierListProvider.notifier).reload(),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (result) => _buildPartnerList(
|
||||
result,
|
||||
isSupplier: true,
|
||||
searchCtrl: _supplierSearchCtrl,
|
||||
onSearchChanged: (v) {
|
||||
_supplierDebounce?.cancel();
|
||||
_supplierDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
ref.read(supplierListProvider.notifier).setKeyword(v);
|
||||
});
|
||||
},
|
||||
onPageChanged: (p) =>
|
||||
ref.read(supplierListProvider.notifier).setPage(p),
|
||||
onAdd: () => _showPartnerDialog(context, isSupplier: true),
|
||||
onEdit: (p) =>
|
||||
_showPartnerDialog(context, isSupplier: true, partner: p),
|
||||
onDelete: (p) => _confirmDelete(context, p, isSupplier: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerTab() {
|
||||
final asyncPartners = ref.watch(customerListProvider);
|
||||
return asyncPartners.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('加载失败:$e',
|
||||
style: const TextStyle(color: AppTheme.danger)),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(customerListProvider.notifier).reload(),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (result) => _buildPartnerList(
|
||||
result,
|
||||
isSupplier: false,
|
||||
searchCtrl: _customerSearchCtrl,
|
||||
onSearchChanged: (v) {
|
||||
_customerDebounce?.cancel();
|
||||
_customerDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
ref.read(customerListProvider.notifier).setKeyword(v);
|
||||
});
|
||||
},
|
||||
onPageChanged: (p) =>
|
||||
ref.read(customerListProvider.notifier).setPage(p),
|
||||
onAdd: () => _showPartnerDialog(context, isSupplier: false),
|
||||
onEdit: (p) =>
|
||||
_showPartnerDialog(context, isSupplier: false, partner: p),
|
||||
onDelete: (p) => _confirmDelete(context, p, isSupplier: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPartnerList(
|
||||
PageResult<Partner> result, {
|
||||
required bool isSupplier,
|
||||
required TextEditingController searchCtrl,
|
||||
required void Function(String) onSearchChanged,
|
||||
required void Function(int) onPageChanged,
|
||||
required VoidCallback onAdd,
|
||||
required void Function(Partner) onEdit,
|
||||
required void Function(Partner) onDelete,
|
||||
}) {
|
||||
final partners = result.data;
|
||||
return DataTableCard(
|
||||
totalCount: filtered.length,
|
||||
page: _suppliersPage,
|
||||
onPageChanged: (p) => setState(() => _suppliersPage = p),
|
||||
totalCount: result.total,
|
||||
page: result.page,
|
||||
onPageChanged: onPageChanged,
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddPartnerDialog(context, isSupplier: true),
|
||||
onPressed: onAdd,
|
||||
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('导出'),
|
||||
label: Text(isSupplier ? '新建' : '新建'),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索供应商名称/编码',
|
||||
prefixIcon: Icon(Icons.search, size: 16),
|
||||
hintStyle: TextStyle(fontSize: 13),
|
||||
controller: searchCtrl,
|
||||
decoration: InputDecoration(
|
||||
hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码',
|
||||
prefixIcon: const Icon(Icons.search, size: 16),
|
||||
hintStyle: const TextStyle(fontSize: 13),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
onChanged: onSearchChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('供应商编码')),
|
||||
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('状态')),
|
||||
DataColumn(label: Text('操作')),
|
||||
columns: [
|
||||
const DataColumn(label: Text('编码')),
|
||||
const DataColumn(label: Text('名称')),
|
||||
const DataColumn(label: Text('联系人')),
|
||||
const DataColumn(label: Text('联系电话')),
|
||||
const DataColumn(label: Text('地址')),
|
||||
const DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: filtered
|
||||
.map((s) => DataRow(cells: [
|
||||
DataCell(Text(s['code'] as String,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary))),
|
||||
DataCell(SizedBox(
|
||||
width: 180,
|
||||
child: Text(s['name'] as String,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
)),
|
||||
DataCell(Text(s['type'] as String)),
|
||||
DataCell(Text(s['contact'] as String)),
|
||||
DataCell(Text(s['phone'] as String)),
|
||||
DataCell(Text('${s['creditDays']}')),
|
||||
DataCell(Text(
|
||||
'¥${s['balance']}',
|
||||
style: TextStyle(
|
||||
color: (s['balance'] as String).startsWith('-')
|
||||
? AppTheme.danger
|
||||
: AppTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
)),
|
||||
DataCell(_StatusChip(s['status'] as String)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('查看',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('对账',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
rows: partners.isEmpty
|
||||
? [
|
||||
DataRow(cells: [
|
||||
const DataCell(SizedBox()),
|
||||
DataCell(Text(isSupplier ? '暂无供应商' : '暂无客户',
|
||||
style:
|
||||
const TextStyle(color: AppTheme.textSecondary))),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
const DataCell(SizedBox()),
|
||||
])
|
||||
]
|
||||
: partners
|
||||
.map((p) => DataRow(
|
||||
cells: [
|
||||
DataCell(Text(p.code ?? '-',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary))),
|
||||
DataCell(SizedBox(
|
||||
width: 180,
|
||||
child: Text(p.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500)),
|
||||
)),
|
||||
DataCell(Text(p.contact ?? '-')),
|
||||
DataCell(Text(p.phone ?? '-')),
|
||||
DataCell(SizedBox(
|
||||
width: 160,
|
||||
child: Text(p.address ?? '-',
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
key: Key('btn_edit_${p.id}'),
|
||||
onPressed: () => onEdit(p),
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
TextButton(
|
||||
key: Key('btn_delete_${p.id}'),
|
||||
onPressed: () => onDelete(p),
|
||||
child: const Text('删除',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.danger)),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerList() {
|
||||
return DataTableCard(
|
||||
totalCount: _customers.length,
|
||||
page: _customersPage,
|
||||
onPageChanged: (p) => setState(() => _customersPage = p),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
_showAddPartnerDialog(context, isSupplier: false),
|
||||
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(),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索客户名称/编码',
|
||||
prefixIcon: Icon(Icons.search, size: 16),
|
||||
hintStyle: TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('客户编码')),
|
||||
DataColumn(label: Text('客户名称')),
|
||||
DataColumn(label: Text('类型')),
|
||||
DataColumn(label: Text('联系人')),
|
||||
DataColumn(label: Text('联系电话')),
|
||||
DataColumn(label: Text('应收余额'), numeric: true),
|
||||
DataColumn(label: Text('状态')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: _customers
|
||||
.map((c) => DataRow(cells: [
|
||||
DataCell(Text(c['code'] as String,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary))),
|
||||
DataCell(SizedBox(
|
||||
width: 180,
|
||||
child: Text(c['name'] as String,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
)),
|
||||
DataCell(Text(c['type'] as String)),
|
||||
DataCell(Text(c['contact'] as String)),
|
||||
DataCell(Text(c['phone'] as String)),
|
||||
DataCell(Text(
|
||||
'¥${c['balance']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
)),
|
||||
DataCell(_StatusChip(c['status'] as String)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('查看',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddPartnerDialog(BuildContext context, {required bool isSupplier}) {
|
||||
final nameCtrl = TextEditingController();
|
||||
final contactCtrl = TextEditingController();
|
||||
final phoneCtrl = TextEditingController();
|
||||
final addressCtrl = TextEditingController();
|
||||
|
||||
void _showPartnerDialog(BuildContext context,
|
||||
{required bool isSupplier, Partner? partner}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => FormDialog(
|
||||
title: isSupplier ? '新增供应商' : '新增客户',
|
||||
width: 520,
|
||||
onConfirm: () {
|
||||
Navigator.of(ctx).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(isSupplier ? '供应商添加成功' : '客户添加成功'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
builder: (ctx) => _PartnerFormDialog(
|
||||
isSupplier: isSupplier,
|
||||
partner: partner,
|
||||
onSaved: () {
|
||||
if (isSupplier) {
|
||||
ref.read(supplierListProvider.notifier).reload();
|
||||
} else {
|
||||
ref.read(customerListProvider.notifier).reload();
|
||||
}
|
||||
},
|
||||
content: Column(
|
||||
children: [
|
||||
_DialogField(
|
||||
label: isSupplier ? '供应商名称' : '客户名称',
|
||||
required: true,
|
||||
child: TextFormField(
|
||||
controller: nameCtrl,
|
||||
decoration: InputDecoration(
|
||||
hintText: isSupplier ? '请输入供应商全称' : '请输入客户名称',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, Partner partner,
|
||||
{required bool isSupplier}) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: Text('确认删除「${partner.name}」?此操作不可恢复。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger,
|
||||
foregroundColor: Colors.white),
|
||||
child: const Text('删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true && mounted) {
|
||||
try {
|
||||
if (isSupplier) {
|
||||
await ref
|
||||
.read(supplierListProvider.notifier)
|
||||
.deletePartner(partner.id);
|
||||
} else {
|
||||
await ref
|
||||
.read(customerListProvider.notifier)
|
||||
.deletePartner(partner.id);
|
||||
}
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('删除成功'),
|
||||
backgroundColor: AppTheme.success));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('删除失败:$e'),
|
||||
backgroundColor: AppTheme.danger));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PartnerFormDialog extends ConsumerStatefulWidget {
|
||||
final bool isSupplier;
|
||||
final Partner? partner;
|
||||
final VoidCallback onSaved;
|
||||
|
||||
const _PartnerFormDialog({
|
||||
required this.isSupplier,
|
||||
this.partner,
|
||||
required this.onSaved,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_PartnerFormDialog> createState() =>
|
||||
_PartnerFormDialogState();
|
||||
}
|
||||
|
||||
class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _codeCtrl;
|
||||
late final TextEditingController _contactCtrl;
|
||||
late final TextEditingController _phoneCtrl;
|
||||
late final TextEditingController _addressCtrl;
|
||||
late final TextEditingController _remarkCtrl;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final p = widget.partner;
|
||||
_nameCtrl = TextEditingController(text: p?.name ?? '');
|
||||
_codeCtrl = TextEditingController(text: p?.code ?? '');
|
||||
_contactCtrl = TextEditingController(text: p?.contact ?? '');
|
||||
_phoneCtrl = TextEditingController(text: p?.phone ?? '');
|
||||
_addressCtrl = TextEditingController(text: p?.address ?? '');
|
||||
_remarkCtrl = TextEditingController(text: p?.remark ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_codeCtrl.dispose();
|
||||
_contactCtrl.dispose();
|
||||
_phoneCtrl.dispose();
|
||||
_addressCtrl.dispose();
|
||||
_remarkCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _saving = true);
|
||||
final data = {
|
||||
'name': _nameCtrl.text.trim(),
|
||||
'type': widget.isSupplier ? 'supplier' : 'customer',
|
||||
if (_codeCtrl.text.trim().isNotEmpty) 'code': _codeCtrl.text.trim(),
|
||||
if (_contactCtrl.text.trim().isNotEmpty)
|
||||
'contact': _contactCtrl.text.trim(),
|
||||
if (_phoneCtrl.text.trim().isNotEmpty) 'phone': _phoneCtrl.text.trim(),
|
||||
if (_addressCtrl.text.trim().isNotEmpty)
|
||||
'address': _addressCtrl.text.trim(),
|
||||
if (_remarkCtrl.text.trim().isNotEmpty) 'remark': _remarkCtrl.text.trim(),
|
||||
};
|
||||
try {
|
||||
final notifier = widget.isSupplier
|
||||
? ref.read(supplierListProvider.notifier)
|
||||
: ref.read(customerListProvider.notifier);
|
||||
if (widget.partner != null) {
|
||||
await notifier.updatePartner(widget.partner!.id, data);
|
||||
} else {
|
||||
await notifier.createPartner(data);
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onSaved();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.partner != null ? '更新成功' : '创建成功'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败:$e'),
|
||||
backgroundColor: AppTheme.danger),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.partner != null;
|
||||
final typeName = widget.isSupplier ? '供应商' : '客户';
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 520,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(isEdit ? '编辑$typeName' : '新建$typeName',
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: InputDecoration(labelText: '$typeName名称'),
|
||||
validator: (v) =>
|
||||
(v == null || v.isEmpty) ? '不能为空' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: _DialogField(
|
||||
label: '联系人',
|
||||
child: TextFormField(
|
||||
controller: contactCtrl,
|
||||
decoration: const InputDecoration(hintText: '联系人姓名'),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _codeCtrl,
|
||||
decoration: const InputDecoration(labelText: '编码'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _DialogField(
|
||||
label: '联系电话',
|
||||
child: TextFormField(
|
||||
controller: phoneCtrl,
|
||||
decoration: const InputDecoration(hintText: '手机/座机'),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _contactCtrl,
|
||||
decoration: const InputDecoration(labelText: '联系人'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_DialogField(
|
||||
label: '地址',
|
||||
child: TextFormField(
|
||||
controller: addressCtrl,
|
||||
decoration: const InputDecoration(hintText: '详细地址'),
|
||||
),
|
||||
),
|
||||
if (isSupplier) ...[
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _phoneCtrl,
|
||||
decoration: const InputDecoration(labelText: '联系电话'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _addressCtrl,
|
||||
decoration: const InputDecoration(labelText: '地址'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _remarkCtrl,
|
||||
decoration: const InputDecoration(labelText: '备注'),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _DialogField(
|
||||
label: '账期(天)',
|
||||
child: TextFormField(
|
||||
initialValue: '30',
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(hintText: '结款账期'),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _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(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DialogField extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget child;
|
||||
final bool required;
|
||||
|
||||
const _DialogField({
|
||||
required this.label,
|
||||
required this.child,
|
||||
this.required = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusChip extends StatelessWidget {
|
||||
final String status;
|
||||
const _StatusChip(this.status);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isActive = status == '合作中';
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? const Color(0xFFE8F5E9)
|
||||
: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
color: isActive ? AppTheme.success : AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user