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
+41 -9
View File
@@ -19,9 +19,13 @@ TRUNCATE TABLE stock_in_items;
TRUNCATE TABLE stock_in_orders;
TRUNCATE TABLE finance_records;
TRUNCATE TABLE number_rules;
TRUNCATE TABLE product_images;
TRUNCATE TABLE partners;
TRUNCATE TABLE warehouses;
TRUNCATE TABLE products;
TRUNCATE TABLE product_spec_options;
TRUNCATE TABLE product_series_options;
TRUNCATE TABLE product_name_options;
TRUNCATE TABLE product_categories;
TRUNCATE TABLE users;
TRUNCATE TABLE shops;
@@ -57,16 +61,44 @@ INSERT INTO product_categories (id, shop_id, name, sort_order, created_at, updat
-- ── 商品 ────────────────────────────────────────────────────
-- id=1~6 白酒, id=7~8 进口烈酒
INSERT INTO products (id, shop_id, code, barcode, name, series, spec, unit, category_id, brand,
INSERT INTO products (id, shop_id, public_id, code, barcode, name, series, spec, unit, category_id, brand,
purchase_price, sale_price, min_stock, remark, created_at, updated_at) VALUES
(1, 1, 'MT-001', '6901234567890', '飞天茅台 53度 500ml', '茅台', '500ml/瓶', '', 1, '贵州茅台', 2350, 2800, 10, '酱香型白酒,53度,飞天系列', NOW(), NOW()),
(2, 1, 'WLY-001', '6902345678901', '五粮液 52度 500ml', '五粮液', '500ml/瓶', '', 1, '宜宾五粮液', 950, 1200, 6, '浓香型白酒,52度,普五系列', NOW(), NOW()),
(3, 1, 'YH-001', '6903456789012', '洋河梦之蓝 M6 500ml', '洋河', '500ml/瓶', '', 1, '江苏洋河', 560, 680, 6, '浓香型,绵柔苏酒代表', NOW(), NOW()),
(4, 1, 'LZ-001', '6904567890123', '泸州老窖 特曲 500ml', '泸州老窖', '500ml/瓶', '', 1, '泸州老窖', 420, 520, 6, '浓香鼻祖,特曲系列', NOW(), NOW()),
(5, 1, 'JNC-001', '6905678901234', '剑南春 水晶剑 500ml', '剑南春', '500ml/瓶', '', 1, '剑南春', 390, 480, 6, '浓香型,绵竹名酒', NOW(), NOW()),
(6, 1, 'LJ-001', '6906789012345', '郎酒 红花郎10 500ml', '郎酒', '500ml/瓶', '', 1, '古蔺郎酒', 320, 420, 6, '酱香型,赤水河畔酿造', NOW(), NOW()),
(7, 1, 'LF-001', '3760093550058', '拉菲古堡 2018 750ml', '波尔多', '750ml/瓶', '', 2, 'Château Lafite', 3800, 5200, 3, '波尔多一级名庄,2018年份', NOW(), NOW()),
(8, 1, 'RTM-001', '3021691010008', '人头马 VSOP 700ml', '人头马', '700ml/瓶', '', 2, 'Rémy Martin', 480, 680, 3, '法国干邑,VSOP级别', NOW(), NOW());
(1, 1, 'a1b2c3d4-0001-0001-0001-000000000001', 'MT-001', '6901234567890', '飞天茅台 53度', '茅台', '500ml/瓶', '', 1, '贵州茅台', 2350, 2800, 10, '酱香型白酒,53度,飞天系列', NOW(), NOW()),
(2, 1, 'a1b2c3d4-0001-0001-0001-000000000002', 'WLY-001', '6902345678901', '五粮液 52度', '五粮液', '500ml/瓶', '', 1, '宜宾五粮液', 950, 1200, 6, '浓香型白酒,52度,普五系列', NOW(), NOW()),
(3, 1, 'a1b2c3d4-0001-0001-0001-000000000003', 'YH-001', '6903456789012', '洋河梦之蓝 M6', '洋河', '500ml/瓶', '', 1, '江苏洋河', 560, 680, 6, '浓香型,绵柔苏酒代表', NOW(), NOW()),
(4, 1, 'a1b2c3d4-0001-0001-0001-000000000004', 'LZ-001', '6904567890123', '泸州老窖 特曲', '泸州老窖', '500ml/瓶', '', 1, '泸州老窖', 420, 520, 6, '浓香鼻祖,特曲系列', NOW(), NOW()),
(5, 1, 'a1b2c3d4-0001-0001-0001-000000000005', 'JNC-001', '6905678901234', '剑南春 水晶剑', '剑南春', '500ml/瓶', '', 1, '剑南春', 390, 480, 6, '浓香型,绵竹名酒', NOW(), NOW()),
(6, 1, 'a1b2c3d4-0001-0001-0001-000000000006', 'LJ-001', '6906789012345', '郎酒 红花郎10', '郎酒', '500ml/瓶', '', 1, '古蔺郎酒', 320, 420, 6, '酱香型,赤水河畔酿造', NOW(), NOW()),
(7, 1, 'a1b2c3d4-0001-0001-0001-000000000007', 'LF-001', '3760093550058', '拉菲古堡 2018', '波尔多', '750ml/瓶', '', 2, 'Château Lafite', 3800, 5200, 3, '波尔多一级名庄,2018年份', NOW(), NOW()),
(8, 1, 'a1b2c3d4-0001-0001-0001-000000000008', 'RTM-001', '3021691010008', '人头马 VSOP', '人头马', '700ml/瓶', '', 2, 'Rémy Martin', 480, 680, 3, '法国干邑,VSOP级别', NOW(), NOW());
-- ── 商品名称选项 ────────────────────────────────────────────
INSERT INTO product_name_options (shop_id, code, name, created_at, updated_at) VALUES
(1, 'NA001', '飞天茅台 53度', NOW(), NOW()),
(1, 'NA002', '五粮液 52度', NOW(), NOW()),
(1, 'NA003', '洋河梦之蓝 M6', NOW(), NOW()),
(1, 'NA004', '泸州老窖 特曲', NOW(), NOW()),
(1, 'NA005', '剑南春 水晶剑', NOW(), NOW()),
(1, 'NA006', '郎酒 红花郎10', NOW(), NOW()),
(1, 'NA007', '拉菲古堡 2018', NOW(), NOW()),
(1, 'NA008', '人头马 VSOP', NOW(), NOW());
-- ── 商品系列选项 ────────────────────────────────────────────
INSERT INTO product_series_options (shop_id, code, name, created_at, updated_at) VALUES
(1, 'SE001', '茅台', NOW(), NOW()),
(1, 'SE002', '五粮液', NOW(), NOW()),
(1, 'SE003', '洋河', NOW(), NOW()),
(1, 'SE004', '泸州老窖', NOW(), NOW()),
(1, 'SE005', '剑南春', NOW(), NOW()),
(1, 'SE006', '郎酒', NOW(), NOW()),
(1, 'SE007', '波尔多', NOW(), NOW()),
(1, 'SE008', '人头马', NOW(), NOW());
-- ── 商品规格选项 ────────────────────────────────────────────
INSERT INTO product_spec_options (shop_id, code, name, quantity, created_at, updated_at) VALUES
(1, 'GG001', '500ml/瓶', 1, NOW(), NOW()),
(1, 'GG002', '750ml/瓶', 1, NOW(), NOW()),
(1, 'GG003', '700ml/瓶', 1, NOW(), NOW());
-- ── 往来单位 ────────────────────────────────────────────────
-- id=1~4 供应商, id=5~7 客户