feat: 公开商品页重设计 + web 上传修复 + 二维码配置

- 公开商品页(/product/:uuid)全面重设计:全宽正方形图片轮播、
  左右滑动切图、点击放大全屏查看、商品参数含描述、页脚贴底
- 修复 Flutter web 文件上传无反应(path→bytes)
- 修复 web 路由空白页(usePathUrlStrategy + 单层 MaterialApp.router)
- 二维码 URL 改为从 STORAGE_PUBLIC_URL 环境变量读取
- 新增 PUBLIC_URL dart-define → AppConfig.publicBaseUrl
- 新增 CI/CD workflows + NAS runner compose 配置
- seed S001 补充商品描述字段

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-30 00:31:11 +08:00
parent 393e227de5
commit 655c1d366e
17 changed files with 738 additions and 157 deletions
@@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -91,16 +92,20 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: false,
withData: true,
);
if (result == null || result.files.isEmpty) return;
final path = result.files.first.path;
if (path == null) return;
final file = result.files.first;
final String? filePath = kIsWeb ? null : file.path;
final fileBytes = file.bytes;
if (filePath == null && fileBytes == null) return;
setState(() => _uploading = true);
try {
final img = await ref
.read(productRepositoryProvider)
.uploadImage(_product!.id, path);
.uploadImage(_product!.id, filePath,
bytes: fileBytes, fileName: file.name);
_updateImages([..._product!.images, img]);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -357,7 +362,7 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
}
Widget _buildPublicLinkSection(Product p) {
final publicUrl = 'https://jiu.51yanmei.com/product/${p.publicId}';
final publicUrl = '${AppConfig.publicBaseUrl}/product/${p.publicId}';
return Card(
elevation: 0,
shape: RoundedRectangleBorder(