feat(shop): 门店 logo 上传 + 侧边栏品牌替换
Deploy / deploy (push) Failing after 8s

- shops 表新增 logo_url 字段,AutoMigrate 自动建列
- 后端新增 POST /api/v1/shop/logo 接口(管理员),图片裁剪为 256×256 JPEG 存储
- UpdateInfo 支持传入 logo_url
- 侧边栏「岩美」替换为门店真实名称 + 自定义 logo
- 无 logo 时显示店名首字文字头像(深蓝底白字)
- 设置页「酒行信息」展示 logo 预览,编辑弹窗新增「更换 Logo」上传按钮
- 修复库存导入计数逻辑:total/imported/updated/errors 四项分别统计
- 库存导入去重改为双路索引(编号 + 名称|系列|规格),避免 key 不一致误判新增
- settings 页导入结果统一显示「重复 X 条」,兼容 skipped 和 updated 两个字段

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-05-24 20:03:43 +08:00
parent 831dbc5959
commit a05f9bd4ec
10 changed files with 320 additions and 93 deletions
+46 -56
View File
@@ -5,8 +5,10 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'dart:async';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/connectivity_provider.dart';
import '../../providers/shop_provider.dart';
import '../../providers/update_provider.dart';
class AppShell extends ConsumerStatefulWidget {
@@ -575,27 +577,31 @@ void _showShopPanel(BuildContext context, AuthUser u, {String version = 'v1.0.0'
);
}
class _ShopButton extends StatelessWidget {
class _ShopButton extends ConsumerWidget {
final AuthUser? user;
final String version;
const _ShopButton({this.user, this.version = 'v1.0.0'});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final shopAsync = ref.watch(shopInfoProvider);
final shopName = shopAsync.valueOrNull?.name ?? user?.shopNo ?? '';
final logoUrl = shopAsync.valueOrNull?.logoUrl ?? '';
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
if (user != null) _showShopPanel(context, user!, version: version);
},
child: const Row(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_YanmeiMark(size: 28),
SizedBox(width: 10),
_ShopLogo(logoUrl: logoUrl, shopName: shopName, size: 28),
const SizedBox(width: 10),
Text(
'岩美',
style: TextStyle(
shopName.isEmpty ? '' : shopName,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
@@ -608,70 +614,54 @@ class _ShopButton extends StatelessWidget {
}
}
/// Brand mark widget — approximates the 岩美 logo SVG without flutter_svg.
/// Dark blue rounded rect, white mountain/wave strokes, bordeaux dot.
class _YanmeiMark extends StatelessWidget {
/// 门店 Logo:有图片显示网络图片,无图片显示店名首字文字头像。
class _ShopLogo extends StatelessWidget {
final String logoUrl;
final String shopName;
final double size;
const _YanmeiMark({this.size = 32});
const _ShopLogo({required this.logoUrl, required this.shopName, this.size = 32});
@override
Widget build(BuildContext context) {
final radius = BorderRadius.circular(size * 0.19);
if (logoUrl.isNotEmpty) {
final fullUrl = logoUrl.startsWith('http') ? logoUrl : '${AppConfig.apiBaseUrl.replaceAll('/api/v1', '')}$logoUrl';
return ClipRRect(
borderRadius: radius,
child: Image.network(
fullUrl,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _initial(radius),
),
);
}
return _initial(radius);
}
Widget _initial(BorderRadius radius) {
final initial = shopName.isNotEmpty ? shopName.characters.first : '';
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: const Color(0xFF0F3057),
borderRadius: BorderRadius.circular(size * 0.19),
borderRadius: radius,
),
child: CustomPaint(
painter: _YanmeiMarkPainter(),
alignment: Alignment.center,
child: Text(
initial,
style: TextStyle(
color: Colors.white,
fontSize: size * 0.5,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _YanmeiMarkPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final paint = Paint()
..color = Colors.white
..strokeWidth = w * 0.055
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
// Mountain/wave path: M14 38 L22 22 L32 32 L42 22 L50 38 (on 64px grid)
final path = Path();
path.moveTo(w * 0.219, h * 0.594);
path.lineTo(w * 0.344, h * 0.344);
path.lineTo(w * 0.500, h * 0.500);
path.lineTo(w * 0.656, h * 0.344);
path.lineTo(w * 0.781, h * 0.594);
canvas.drawPath(path, paint);
// Horizontal baseline: M12 46 L52 46 (on 64px grid)
canvas.drawLine(
Offset(w * 0.1875, h * 0.719),
Offset(w * 0.8125, h * 0.719),
paint,
);
// Bordeaux dot: circle cx=32 cy=52 r=2.4 (on 64px grid)
canvas.drawCircle(
Offset(w * 0.500, h * 0.859),
w * 0.042,
Paint()
..color = const Color(0xFFC97B86)
..style = PaintingStyle.fill,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class _InfoRow extends StatelessWidget {
final IconData icon;
final String label;