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
+2
View File
@@ -0,0 +1,2 @@
export 'export_util_stub.dart'
if (dart.library.js_interop) 'export_util_web.dart';
@@ -0,0 +1,33 @@
import 'dart:io';
import 'package:excel/excel.dart';
import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart';
void exportExcel({
required String filename,
required List<String> headers,
required List<List<dynamic>> rows,
}) async {
final excel = Excel.createExcel();
final sheet = excel['Sheet1'];
sheet.appendRow(headers.map((h) => TextCellValue(h)).toList());
for (final row in rows) {
final cells = row.map<CellValue?>((v) {
if (v == null) return TextCellValue('');
if (v is double) return DoubleCellValue(v);
if (v is int) return IntCellValue(v);
return TextCellValue(v.toString());
}).toList();
sheet.appendRow(cells);
}
final bytes = excel.encode();
if (bytes == null) return;
final dir = await getDownloadsDirectory() ?? await getTemporaryDirectory();
final file = File('${dir.path}/$filename.xlsx');
await file.writeAsBytes(bytes);
await launchUrl(Uri.file(file.path));
}
@@ -0,0 +1,42 @@
import 'dart:typed_data';
import 'dart:js_interop';
import 'package:excel/excel.dart';
import 'package:web/web.dart' as web;
void exportExcel({
required String filename,
required List<String> headers,
required List<List<dynamic>> rows,
}) {
final excel = Excel.createExcel();
final sheet = excel['Sheet1'];
sheet.appendRow(headers.map((h) => TextCellValue(h)).toList());
for (final row in rows) {
final cells = row.map<CellValue?>((v) {
if (v == null) return TextCellValue('');
if (v is double) return DoubleCellValue(v);
if (v is int) return IntCellValue(v);
return TextCellValue(v.toString());
}).toList();
sheet.appendRow(cells);
}
final bytes = excel.encode();
if (bytes == null) return;
final uint8Array = Uint8List.fromList(bytes).toJS;
final blob = web.Blob(
<JSAny>[uint8Array].toJS,
web.BlobPropertyBag(
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
);
final url = web.URL.createObjectURL(blob);
final anchor = web.document.createElement('a') as web.HTMLAnchorElement
..href = url
..download = '$filename.xlsx'
..click();
web.URL.revokeObjectURL(url);
anchor.remove();
}