feat: 商品详情页、XLS导入修复、分页选择器、导出功能

后端:
- 新增 product_images 表,支持每商品最多5张图(服务端压缩至1200px/JPEG85%)
- products 表新增 public_id(UUID)、description 字段
- 新增商品详情接口、二维码接口、公开商品接口(无鉴权)
- 修复 XLS 导入:OLE2 magic bytes 检测 + 临时文件解析,兼容 extrame/xls
- 修复商品/名称/系列/规格三张表导入数据为0(LastCol()=0 bug)
- 所有导入接口返回 total/imported/skipped 统计
- config 新增 StorageConfig,支持 STORAGE_* 环境变量覆盖
- 种子数据修复:products 补 public_id、新增 product_images TRUNCATE、schema.sql 表名修正

前端:
- 商品详情页:图片上传/删除、描述内联编辑、二维码弹窗、公开链接复制
- 公开商品页:无鉴权路由 /product/:public_id,Flutter Web SPA
- 商品详情列表(批次追踪)商品名超链接跳转详情页
- 导航「商品管理」改名「商品详情」
- 所有列表表格新增每页条数选择(10/20/50/100)
- 表格列头内嵌筛选(FilterableColumnHeader)
- 导出 Excel 功能(入库/出库/库存/财务/批次/往来单位)
- 网络恢复自动刷新 + 离线缓存展示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-27 00:29:51 +08:00
parent 5dd7c07138
commit 393e227de5
70 changed files with 4993 additions and 1169 deletions
@@ -7,6 +7,7 @@ import '../../models/partner.dart';
import '../../providers/partner_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
class PartnersScreen extends ConsumerStatefulWidget {
const PartnersScreen({super.key});
@@ -78,6 +79,8 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
},
onPageChanged: (p) =>
ref.read(supplierListProvider.notifier).setPage(p),
onPageSizeChanged: (s) =>
ref.read(supplierListProvider.notifier).setPageSize(s),
onAdd: () => _showPartnerDialog(context, isSupplier: true),
onEdit: (p) =>
_showPartnerDialog(context, isSupplier: true, partner: p),
@@ -119,6 +122,8 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
},
onPageChanged: (p) =>
ref.read(customerListProvider.notifier).setPage(p),
onPageSizeChanged: (s) =>
ref.read(customerListProvider.notifier).setPageSize(s),
onAdd: () => _showPartnerDialog(context, isSupplier: false),
onEdit: (p) =>
_showPartnerDialog(context, isSupplier: false, partner: p),
@@ -133,6 +138,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
required TextEditingController searchCtrl,
required void Function(String) onSearchChanged,
required void Function(int) onPageChanged,
required void Function(int) onPageSizeChanged,
required VoidCallback onAdd,
required void Function(Partner) onEdit,
required void Function(Partner) onDelete,
@@ -141,7 +147,9 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
return DataTableCard(
totalCount: result.total,
page: result.page,
pageSize: result.pageSize,
onPageChanged: onPageChanged,
onPageSizeChanged: onPageSizeChanged,
toolbar: Row(
children: [
ElevatedButton.icon(
@@ -149,6 +157,19 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
icon: const Icon(Icons.add, size: 16),
label: Text(isSupplier ? '新建' : '新建'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () => exportExcel(
filename: isSupplier ? '供应商列表' : '客户列表',
headers: ['编码', '名称', '联系人', '电话', '地址', '备注'],
rows: partners.map((p) => [
p.code ?? '', p.name, p.contact ?? '',
p.phone ?? '', p.address ?? '', p.remark ?? '',
]).toList(),
),
icon: const Icon(Icons.download, size: 16),
label: const Text('导出'),
),
const Spacer(),
SizedBox(
width: 200,
@@ -170,6 +191,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
const DataColumn(label: Text('联系人')),
const DataColumn(label: Text('联系电话')),
const DataColumn(label: Text('地址')),
const DataColumn(label: Text('状态')),
const DataColumn(label: Text('操作')),
],
rows: partners.isEmpty
@@ -183,6 +205,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: partners
@@ -207,6 +230,15 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
child: Text(p.address ?? '-',
overflow: TextOverflow.ellipsis),
)),
DataCell(Text(
p.status == 'enabled' ? '正常' : '禁用',
style: TextStyle(
fontSize: 13,
color: p.status == 'enabled'
? AppTheme.success
: AppTheme.textSecondary,
),
)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [