feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服 pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验; 登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段, 已记 CONTRACT,screens.mjs 留存根) - ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/ DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态 - 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、 h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、 BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast) - 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后 失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览 - 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/ SelectProductDialog/tabStateProvider - golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打; 修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -49,13 +50,20 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() { _loading = true; _error = null; });
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final resp = await _dio.get(
|
||||
'${AppConfig.apiBaseUrl}/public/products/${widget.publicId}',
|
||||
);
|
||||
final data = (resp.data as Map<String, dynamic>)['data'] as Map<String, dynamic>;
|
||||
setState(() { _data = data; _loading = false; });
|
||||
final data =
|
||||
(resp.data as Map<String, dynamic>)['data'] as Map<String, dynamic>;
|
||||
setState(() {
|
||||
_data = data;
|
||||
_loading = false;
|
||||
});
|
||||
// 更新浏览器标签标题(Flutter Web 会写 document.title)
|
||||
final name = data['name'] as String? ?? '';
|
||||
if (name.isNotEmpty) {
|
||||
@@ -64,7 +72,10 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() { _error = e.toString(); _loading = false; });
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +90,9 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
title: title,
|
||||
hint: hint,
|
||||
onSubmit: (msg) async {
|
||||
final shopNo = (_data?['shop'] as Map<String, dynamic>?)?['code'] as String? ?? '';
|
||||
final shopNo =
|
||||
(_data?['shop'] as Map<String, dynamic>?)?['code'] as String? ??
|
||||
'';
|
||||
final productName = _data?['name'] as String? ?? '';
|
||||
final productCode = _data?['code'] as String? ?? '';
|
||||
final productInfo = [
|
||||
@@ -107,7 +120,9 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
if (_loading) {
|
||||
return const Scaffold(
|
||||
backgroundColor: _kPaper,
|
||||
body: Center(child: CircularProgressIndicator(color: _kBurgundy, strokeWidth: 2)),
|
||||
body: Center(
|
||||
child:
|
||||
CircularProgressIndicator(color: _kBurgundy, strokeWidth: 2)),
|
||||
);
|
||||
}
|
||||
if (_error != null || _data == null) {
|
||||
@@ -120,26 +135,33 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 56, height: 56,
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFDECEC),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.error_outline, size: 28, color: Color(0xFFD14343)),
|
||||
child: const Icon(LucideIcons.circleAlert,
|
||||
size: 28, color: Color(0xFFD14343)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('商品不存在或已下架',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: _kInkDeep)),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _kInkDeep)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('请检查二维码是否完整,或联系出售方确认',
|
||||
style: TextStyle(fontSize: 13, color: _kTextMid), textAlign: TextAlign.center),
|
||||
style: TextStyle(fontSize: 13, color: _kTextMid),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _load,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _kBurgundy,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
@@ -151,8 +173,11 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
}
|
||||
|
||||
final d = _data!;
|
||||
final images = (d['images'] as List<dynamic>? ?? []).cast<Map<String, dynamic>>();
|
||||
final imageUrls = images.map((img) => AppConfig.baseUrl + (img['url'] as String)).toList();
|
||||
final images =
|
||||
(d['images'] as List<dynamic>? ?? []).cast<Map<String, dynamic>>();
|
||||
final imageUrls = images
|
||||
.map((img) => AppConfig.baseUrl + (img['url'] as String))
|
||||
.toList();
|
||||
final name = d['name'] as String? ?? '';
|
||||
final series = d['series'] as String? ?? '';
|
||||
final spec = d['spec'] as String? ?? '';
|
||||
@@ -161,15 +186,17 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
final description = d['description'] as String? ?? '';
|
||||
final descriptionTitle = d['description_title'] as String? ?? '商品介绍';
|
||||
final descriptionKeywords = (d['description_keywords'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ?? const <String>[];
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const <String>[];
|
||||
final salePrice = (d['sale_price'] as num?)?.toDouble() ?? 0.0;
|
||||
final origin = d['origin'] as String? ?? '';
|
||||
final shelfLife = d['shelf_life'] as String? ?? '无限期(适饮)';
|
||||
final storage = d['storage'] as String? ?? '阴凉干燥、避光保存';
|
||||
final shop = d['shop'] as Map<String, dynamic>?;
|
||||
final batch = d['batch'] as Map<String, dynamic>?;
|
||||
final quickSpecs = _parseQuickSpecs(spec, batch?['production_date'] as String?);
|
||||
final quickSpecs =
|
||||
_parseQuickSpecs(spec, batch?['production_date'] as String?);
|
||||
final productionDate = batch?['production_date'] as String?;
|
||||
|
||||
return Scaffold(
|
||||
@@ -182,7 +209,9 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
slivers: [
|
||||
// 1. Gallery
|
||||
SliverToBoxAdapter(
|
||||
child: _HeroGallery(imageUrls: imageUrls, shopName: shop?['name'] as String? ?? ''),
|
||||
child: _HeroGallery(
|
||||
imageUrls: imageUrls,
|
||||
shopName: shop?['name'] as String? ?? ''),
|
||||
),
|
||||
// 2. Verified ribbon
|
||||
SliverToBoxAdapter(
|
||||
@@ -268,7 +297,8 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
||||
final items = <({String value, String unit, String label})>[];
|
||||
|
||||
// Alcohol degree: 53度 / 53° / 53%vol
|
||||
final degReg = RegExp(r'(\d+(?:\.\d+)?)\s*(?:度|°|%vol)', caseSensitive: false);
|
||||
final degReg =
|
||||
RegExp(r'(\d+(?:\.\d+)?)\s*(?:度|°|%vol)', caseSensitive: false);
|
||||
final degM = degReg.firstMatch(spec);
|
||||
if (degM != null) {
|
||||
items.add((value: degM.group(1)!, unit: '°', label: '酒精度'));
|
||||
@@ -344,7 +374,8 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
// Golden halo glow (behind product)
|
||||
Center(
|
||||
child: Container(
|
||||
width: 240, height: 240,
|
||||
width: 240,
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
@@ -369,18 +400,26 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
child: Image.network(
|
||||
urls[i],
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => _EmptyGalleryContent(shopName: widget.shopName),
|
||||
errorBuilder: (_, __, ___) =>
|
||||
_EmptyGalleryContent(shopName: widget.shopName),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Top gradient scrim
|
||||
Positioned(
|
||||
top: 0, left: 0, right: 0, height: 64,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 64,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter, end: Alignment.bottomCenter,
|
||||
colors: [Colors.black.withValues(alpha: 0.45), Colors.transparent],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.45),
|
||||
Colors.transparent
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -388,43 +427,50 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
// "点击放大" hint (top right)
|
||||
if (urls.isNotEmpty)
|
||||
Positioned(
|
||||
top: 52, right: 14,
|
||||
top: 52,
|
||||
right: 14,
|
||||
child: _GlassChip(
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.zoom_in, size: 12, color: Colors.white),
|
||||
Icon(LucideIcons.zoomIn, size: 12, color: Colors.white),
|
||||
SizedBox(width: 4),
|
||||
Text('点击放大', style: TextStyle(fontSize: 11, color: Colors.white)),
|
||||
Text('点击放大',
|
||||
style: TextStyle(fontSize: 11, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom: dots + page counter
|
||||
Positioned(
|
||||
bottom: 16, left: 0, right: 0,
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
// Dots
|
||||
...List.generate(math.max(urls.length, 1), (i) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: i == _current ? 18 : 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.only(right: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: i == _current
|
||||
? Colors.white.withValues(alpha: 0.95)
|
||||
: Colors.white.withValues(alpha: 0.30),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
)),
|
||||
...List.generate(
|
||||
math.max(urls.length, 1),
|
||||
(i) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: i == _current ? 18 : 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.only(right: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: i == _current
|
||||
? Colors.white.withValues(alpha: 0.95)
|
||||
: Colors.white.withValues(alpha: 0.30),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
)),
|
||||
const Spacer(),
|
||||
if (urls.length > 1)
|
||||
Text(
|
||||
'${_current + 1} / ${urls.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Color(0x88FFFFFF),
|
||||
fontSize: 11,
|
||||
color: Color(0x88FFFFFF),
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
@@ -436,7 +482,9 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
// Thumbnail strip
|
||||
if (urls.length > 1)
|
||||
Positioned(
|
||||
bottom: 44, left: 0, right: 0,
|
||||
bottom: 44,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
@@ -451,7 +499,8 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
curve: Curves.easeInOut),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
width: 44, height: 44,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
@@ -463,7 +512,8 @@ class _HeroGalleryState extends State<_HeroGallery> {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: Image.network(urls[i], fit: BoxFit.cover,
|
||||
child: Image.network(urls[i],
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const SizedBox()),
|
||||
),
|
||||
),
|
||||
@@ -493,14 +543,18 @@ class _EmptyGalleryContent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 64, height: 64,
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F3057),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(initial,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 28, fontWeight: FontWeight.w700)),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -554,13 +608,14 @@ class _VerifiedRibbon extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 28, height: 28,
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: _kSuccessLight,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFFB4E2C5)),
|
||||
),
|
||||
child: const Icon(Icons.check, size: 15, color: _kSuccess),
|
||||
child: const Icon(LucideIcons.check, size: 15, color: _kSuccess),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -572,7 +627,8 @@ class _VerifiedRibbon extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: detail,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, color: Color(0xFF353C48)),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF353C48)),
|
||||
),
|
||||
],
|
||||
))
|
||||
@@ -598,7 +654,7 @@ class _VerifiedRibbon extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, size: 18, color: _kTextMid),
|
||||
const Icon(LucideIcons.chevronRight, size: 18, color: _kTextMid),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -613,9 +669,8 @@ class _PriceBadge extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final priceStr = price % 1 == 0
|
||||
? price.toInt().toString()
|
||||
: price.toStringAsFixed(2);
|
||||
final priceStr =
|
||||
price % 1 == 0 ? price.toInt().toString() : price.toStringAsFixed(2);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
@@ -624,8 +679,7 @@ class _PriceBadge extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('建议零售价',
|
||||
style: TextStyle(fontSize: 13, color: _kTextMid)),
|
||||
const Text('建议零售价', style: TextStyle(fontSize: 13, color: _kTextMid)),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'¥$priceStr',
|
||||
@@ -647,8 +701,11 @@ class _PriceBadge extends StatelessWidget {
|
||||
class _TitleBlock extends StatelessWidget {
|
||||
final String name, brand, series, spec, unit;
|
||||
const _TitleBlock({
|
||||
required this.name, required this.brand, required this.series,
|
||||
required this.spec, required this.unit,
|
||||
required this.name,
|
||||
required this.brand,
|
||||
required this.series,
|
||||
required this.spec,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -675,17 +732,25 @@ class _TitleBlock extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(width: 16, height: 1, color: _kBurgundy.withValues(alpha: 0.7)),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 1,
|
||||
color: _kBurgundy.withValues(alpha: 0.7)),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
separatorText,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, letterSpacing: 0.28,
|
||||
color: _kBurgundy, fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.28,
|
||||
color: _kBurgundy,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Container(width: 16, height: 1, color: _kBurgundy.withValues(alpha: 0.7)),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 1,
|
||||
color: _kBurgundy.withValues(alpha: 0.7)),
|
||||
],
|
||||
),
|
||||
if (separatorText.isNotEmpty) const SizedBox(height: 14),
|
||||
@@ -694,8 +759,10 @@ class _TitleBlock extends StatelessWidget {
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 38, fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.08, color: _kInkDeep,
|
||||
fontSize: 38,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.08,
|
||||
color: _kInkDeep,
|
||||
height: 1.15,
|
||||
),
|
||||
),
|
||||
@@ -706,8 +773,10 @@ class _TitleBlock extends StatelessWidget {
|
||||
_keySpec(spec),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 17, fontWeight: FontWeight.w500,
|
||||
color: _kBurgundy, letterSpacing: 0.04,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _kBurgundy,
|
||||
letterSpacing: 0.04,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -718,7 +787,9 @@ class _TitleBlock extends StatelessWidget {
|
||||
specLine,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: _kTextMid, letterSpacing: 0.03,
|
||||
fontSize: 13,
|
||||
color: _kTextMid,
|
||||
letterSpacing: 0.03,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -726,7 +797,8 @@ class _TitleBlock extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8, runSpacing: 6,
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
if (series.isNotEmpty)
|
||||
_TitleTag(label: series, style: _TitleTagStyle.burgundy),
|
||||
@@ -743,7 +815,8 @@ class _TitleBlock extends StatelessWidget {
|
||||
// Extract "53° · 酱香型白酒" from spec
|
||||
String _keySpec(String spec) {
|
||||
final parts = <String>[];
|
||||
final degM = RegExp(r'(\d+(?:\.\d+)?)\s*(?:度|°|%vol)', caseSensitive: false).firstMatch(spec);
|
||||
final degM = RegExp(r'(\d+(?:\.\d+)?)\s*(?:度|°|%vol)', caseSensitive: false)
|
||||
.firstMatch(spec);
|
||||
if (degM != null) parts.add('${degM.group(1)!}°');
|
||||
final xiangM = RegExp(r'([清浓酱兼馥凤]香(?:型(?:白酒)?)?)').firstMatch(spec);
|
||||
if (xiangM != null) parts.add(xiangM.group(1)!);
|
||||
@@ -753,7 +826,8 @@ class _TitleBlock extends StatelessWidget {
|
||||
String _buildSpecLine(String spec, String unit, String series) {
|
||||
// Remove parts already captured in _keySpec
|
||||
var s = spec
|
||||
.replaceAll(RegExp(r'\d+(?:\.\d+)?\s*(?:度|°|%vol)', caseSensitive: false), '')
|
||||
.replaceAll(
|
||||
RegExp(r'\d+(?:\.\d+)?\s*(?:度|°|%vol)', caseSensitive: false), '')
|
||||
.replaceAll(RegExp(r'[清浓酱兼馥凤]香(?:型(?:白酒)?)?'), '')
|
||||
.replaceAll(RegExp(r'^[·\s]+|[·\s]+$'), '')
|
||||
.trim();
|
||||
@@ -787,10 +861,14 @@ class _TitleTag extends StatelessWidget {
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(4)),
|
||||
decoration:
|
||||
BoxDecoration(color: bg, borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(label,
|
||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600,
|
||||
color: fg, letterSpacing: 0.06)),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: fg,
|
||||
letterSpacing: 0.06)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -827,8 +905,10 @@ class _QuickSpecs extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
text: it.value,
|
||||
style: const TextStyle(
|
||||
fontSize: 22, fontWeight: FontWeight.w700,
|
||||
color: _kInkDeep, height: 1,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _kInkDeep,
|
||||
height: 1,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
children: it.unit.isNotEmpty
|
||||
@@ -836,8 +916,9 @@ class _QuickSpecs extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: it.unit,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, fontWeight: FontWeight.w500,
|
||||
color: _kTextMid),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _kTextMid),
|
||||
)
|
||||
]
|
||||
: null,
|
||||
@@ -887,28 +968,35 @@ class _DescriptionSection extends StatelessWidget {
|
||||
Text(
|
||||
body,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: _kInkSoft, height: 1.85, letterSpacing: 0.02),
|
||||
fontSize: 14,
|
||||
color: _kInkSoft,
|
||||
height: 1.85,
|
||||
letterSpacing: 0.02),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 8, runSpacing: 6,
|
||||
children: keywords.map((kw) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F3EF),
|
||||
border: Border.all(color: _kBorderSub),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(kw, style: const TextStyle(
|
||||
fontSize: 11, color: _kTextMid)),
|
||||
)).toList(),
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: keywords
|
||||
.map((kw) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F3EF),
|
||||
border: Border.all(color: _kBorderSub),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(kw,
|
||||
style:
|
||||
const TextStyle(fontSize: 11, color: _kTextMid)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ── Authenticity Card ─────────────────────────────────────────────────
|
||||
@@ -962,8 +1050,7 @@ class _AuthenticityCard extends StatelessWidget {
|
||||
_AuthRow(label: '入库日期', value: inStockDate),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
if (qty > 0)
|
||||
_AuthRow(label: '批次数量', value: '$qty 瓶'),
|
||||
if (qty > 0) _AuthRow(label: '批次数量', value: '$qty 瓶'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -997,11 +1084,14 @@ class _AuthRow extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 10, color: _kTextMid, letterSpacing: 0.05)),
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: _kTextMid, letterSpacing: 0.05)),
|
||||
const SizedBox(height: 3),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: _kInkSoft, fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
color: _kInkSoft,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: mono ? 'monospace' : null,
|
||||
letterSpacing: mono ? 0.5 : 0,
|
||||
)),
|
||||
@@ -1016,7 +1106,8 @@ class _AuthStamp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 84, height: 84,
|
||||
width: 84,
|
||||
height: 84,
|
||||
child: CustomPaint(painter: _StampPainter()),
|
||||
);
|
||||
}
|
||||
@@ -1040,7 +1131,8 @@ class _StampPainter extends CustomPainter {
|
||||
canvas.drawCircle(center, innerR, ringPaint);
|
||||
|
||||
// Circular text "防伪验证 · VERIFIED · "
|
||||
_drawCircularText(canvas, '防 伪 验 证 · V E R I F I E D · ', center, innerR - 7);
|
||||
_drawCircularText(
|
||||
canvas, '防 伪 验 证 · V E R I F I E D · ', center, innerR - 7);
|
||||
|
||||
// Center: checkmark
|
||||
final checkPaint = Paint()
|
||||
@@ -1060,8 +1152,10 @@ class _StampPainter extends CustomPainter {
|
||||
text: const TextSpan(
|
||||
text: '已验证',
|
||||
style: TextStyle(
|
||||
fontSize: 8.5, fontWeight: FontWeight.w700,
|
||||
color: _kBurgundy, letterSpacing: 1.0,
|
||||
fontSize: 8.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _kBurgundy,
|
||||
letterSpacing: 1.0,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
@@ -1069,7 +1163,8 @@ class _StampPainter extends CustomPainter {
|
||||
tp.paint(canvas, Offset(cx - tp.width / 2, cy + 10));
|
||||
}
|
||||
|
||||
void _drawCircularText(Canvas canvas, String text, Offset center, double radius) {
|
||||
void _drawCircularText(
|
||||
Canvas canvas, String text, Offset center, double radius) {
|
||||
final charCount = text.length;
|
||||
final angleStep = (2 * math.pi) / charCount;
|
||||
const startAngle = -math.pi / 2;
|
||||
@@ -1082,8 +1177,10 @@ class _StampPainter extends CustomPainter {
|
||||
text: TextSpan(
|
||||
text: text[i],
|
||||
style: const TextStyle(
|
||||
fontSize: 6.5, color: _kBurgundy,
|
||||
fontWeight: FontWeight.w600, letterSpacing: 0,
|
||||
fontSize: 6.5,
|
||||
color: _kBurgundy,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
@@ -1102,9 +1199,9 @@ class _StampPainter extends CustomPainter {
|
||||
class _ParamsCard extends StatelessWidget {
|
||||
final String publicId, spec, brand, series, unit;
|
||||
final String? productionDate;
|
||||
final String origin; // 空串表示无产地数据,不显示该行
|
||||
final String origin; // 空串表示无产地数据,不显示该行
|
||||
final String shelfLife; // 后端已回填默认值
|
||||
final String storage; // 后端已回填默认值
|
||||
final String storage; // 后端已回填默认值
|
||||
const _ParamsCard({
|
||||
required this.publicId,
|
||||
required this.spec,
|
||||
@@ -1120,8 +1217,10 @@ class _ParamsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Parse alcohol and volume from spec
|
||||
final degM = RegExp(r'(\d+(?:\.\d+)?)\s*(?:度|°|%vol)', caseSensitive: false).firstMatch(spec);
|
||||
final mlM = RegExp(r'(\d+(?:\.\d+)?)\s*ml', caseSensitive: false).firstMatch(spec);
|
||||
final degM = RegExp(r'(\d+(?:\.\d+)?)\s*(?:度|°|%vol)', caseSensitive: false)
|
||||
.firstMatch(spec);
|
||||
final mlM =
|
||||
RegExp(r'(\d+(?:\.\d+)?)\s*ml', caseSensitive: false).firstMatch(spec);
|
||||
final xiangM = RegExp(r'([清浓酱兼馥凤]香(?:型(?:白酒)?)?)').firstMatch(spec);
|
||||
|
||||
final rows = <({String label, String value, bool mono})>[
|
||||
@@ -1129,11 +1228,14 @@ class _ParamsCard extends StatelessWidget {
|
||||
if (brand.isNotEmpty) (label: '品牌', value: brand, mono: false),
|
||||
if (series.isNotEmpty) (label: '系列', value: series, mono: false),
|
||||
if (xiangM != null) (label: '香型', value: xiangM.group(1)!, mono: false),
|
||||
if (degM != null) (label: '酒精度', value: '${degM.group(1)!}% vol', mono: false),
|
||||
if (mlM != null) (label: '净含量', value: '${mlM.group(1)!} ml', mono: false),
|
||||
if (degM != null)
|
||||
(label: '酒精度', value: '${degM.group(1)!}% vol', mono: false),
|
||||
if (mlM != null)
|
||||
(label: '净含量', value: '${mlM.group(1)!} ml', mono: false),
|
||||
if (spec.isNotEmpty) (label: '规格', value: spec, mono: false),
|
||||
if (origin.isNotEmpty) (label: '产地', value: origin, mono: false),
|
||||
if (productionDate != null) (label: '生产日期', value: productionDate!, mono: false),
|
||||
if (productionDate != null)
|
||||
(label: '生产日期', value: productionDate!, mono: false),
|
||||
(label: '保质期', value: shelfLife, mono: false),
|
||||
(label: '储存方式', value: storage, mono: false),
|
||||
];
|
||||
@@ -1165,13 +1267,18 @@ class _ParamsCard extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text(r.label,
|
||||
style: const TextStyle(fontSize: 12, color: _kTextMid, letterSpacing: 0.02)),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _kTextMid,
|
||||
letterSpacing: 0.02)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(r.value,
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: _kInkSoft, fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
color: _kInkSoft,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: r.mono ? 'monospace' : null,
|
||||
letterSpacing: r.mono ? 0.5 : 0.01,
|
||||
)),
|
||||
@@ -1198,12 +1305,17 @@ class _SectionHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(width: 3, height: 16, color: _kBurgundy,
|
||||
Container(
|
||||
width: 3,
|
||||
height: 16,
|
||||
color: _kBurgundy,
|
||||
margin: const EdgeInsets.only(right: 10)),
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w700,
|
||||
color: _kInkDeep, letterSpacing: 0.06)),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _kInkDeep,
|
||||
letterSpacing: 0.06)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1242,7 +1354,8 @@ class _ShopCard extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52, height: 52,
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0A1F3B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -1250,7 +1363,9 @@ class _ShopCard extends StatelessWidget {
|
||||
child: Center(
|
||||
child: Text(avatarText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 16, fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.04)),
|
||||
),
|
||||
),
|
||||
@@ -1259,13 +1374,17 @@ class _ShopCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name, style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600, color: _kInkDeep,
|
||||
letterSpacing: 0.04)),
|
||||
Text(name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _kInkDeep,
|
||||
letterSpacing: 0.04)),
|
||||
if (code.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text('门店编号 $code',
|
||||
style: const TextStyle(fontSize: 12, color: _kTextMid)),
|
||||
style:
|
||||
const TextStyle(fontSize: 12, color: _kTextMid)),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -1275,44 +1394,54 @@ class _ShopCard extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
Container(height: 1, color: _kBorderSub),
|
||||
const SizedBox(height: 14),
|
||||
if (address.isNotEmpty) _ShopInfoRow(icon: Icons.location_on_outlined, text: address),
|
||||
if (phone.isNotEmpty) _ShopInfoRow(icon: Icons.phone_outlined, text: phone),
|
||||
if (address.isNotEmpty)
|
||||
_ShopInfoRow(icon: LucideIcons.mapPin, text: address),
|
||||
if (phone.isNotEmpty)
|
||||
_ShopInfoRow(icon: LucideIcons.phone, text: phone),
|
||||
_ShopInfoRow(
|
||||
icon: Icons.access_time_outlined,
|
||||
icon: LucideIcons.clock,
|
||||
text: '营业时间 ${hours.isNotEmpty ? hours : "09:30 – 22:00"}',
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity, height: 44,
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
child: ElevatedButton(
|
||||
onPressed: code.isNotEmpty
|
||||
? () => context.push(
|
||||
'/shop/$code?shopName=${Uri.encodeComponent(name)}')
|
||||
? () => context
|
||||
.push('/shop/$code?shopName=${Uri.encodeComponent(name)}')
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0A1F3B),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('查看本店其他商品 →',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.04)),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.04)),
|
||||
),
|
||||
),
|
||||
if (wechatId.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity, height: 40,
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _showWechatDialog(context, wechatId),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: _kInkDeep,
|
||||
side: const BorderSide(color: _kBorder, width: 1.5),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
icon: const Icon(Icons.person_outline, size: 16),
|
||||
icon: const Icon(LucideIcons.user, size: 16),
|
||||
label: const Text('添加门店微信',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
||||
style:
|
||||
TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1326,19 +1455,23 @@ void _showWechatDialog(BuildContext context, String wechatId) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('门店微信号', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
title: const Text('门店微信号',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.wechat, size: 48, color: Color(0xFF07C160)),
|
||||
const Icon(LucideIcons.messageCircle,
|
||||
size: 48, color: Color(0xFF07C160)),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText(
|
||||
wechatId,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: 0.5),
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: 0.5),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('长按上方微信号可复制', style: TextStyle(fontSize: 12, color: _kTextMid)),
|
||||
const Text('长按上方微信号可复制',
|
||||
style: TextStyle(fontSize: 12, color: _kTextMid)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -1367,7 +1500,8 @@ class _ShopInfoRow extends StatelessWidget {
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: const TextStyle(fontSize: 13, color: _kInkSoft, height: 1.5)),
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: _kInkSoft, height: 1.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1393,7 +1527,8 @@ class _Footer extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 20, height: 20,
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
@@ -1403,14 +1538,18 @@ class _Footer extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF2563AC).withValues(alpha: 0.30),
|
||||
blurRadius: 4, offset: const Offset(0, 2)),
|
||||
color: const Color(0xFF2563AC).withValues(alpha: 0.30),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: const Center(
|
||||
child: Text('岩',
|
||||
style: TextStyle(color: Colors.white, fontSize: 11,
|
||||
fontWeight: FontWeight.w800, letterSpacing: -0.5)),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -1421,7 +1560,9 @@ class _Footer extends StatelessWidget {
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '岩美酒库管理系统',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: Color(0xFF353C48)),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF353C48)),
|
||||
),
|
||||
TextSpan(text: ' 提供'),
|
||||
],
|
||||
@@ -1456,9 +1597,11 @@ class _FooterTextLink extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final active = url != null || onTap != null;
|
||||
return GestureDetector(
|
||||
onTap: onTap ?? (url != null
|
||||
? () => launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication)
|
||||
: null),
|
||||
onTap: onTap ??
|
||||
(url != null
|
||||
? () => launchUrl(Uri.parse(url!),
|
||||
mode: LaunchMode.externalApplication)
|
||||
: null),
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
@@ -1489,7 +1632,8 @@ class _FeedbackDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final String hint;
|
||||
final Future<void> Function(String) onSubmit;
|
||||
const _FeedbackDialog({required this.title, required this.hint, required this.onSubmit});
|
||||
const _FeedbackDialog(
|
||||
{required this.title, required this.hint, required this.onSubmit});
|
||||
|
||||
@override
|
||||
State<_FeedbackDialog> createState() => _FeedbackDialogState();
|
||||
@@ -1511,7 +1655,11 @@ class _FeedbackDialogState extends State<_FeedbackDialog> {
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
await widget.onSubmit(_ctrl.text.trim());
|
||||
if (mounted) setState(() { _done = true; _submitting = false; });
|
||||
if (mounted)
|
||||
setState(() {
|
||||
_done = true;
|
||||
_submitting = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
@@ -1525,7 +1673,7 @@ class _FeedbackDialogState extends State<_FeedbackDialog> {
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline, size: 48, color: _kSuccess),
|
||||
Icon(LucideIcons.circleCheck, size: 48, color: _kSuccess),
|
||||
SizedBox(height: 12),
|
||||
Text('提交成功,感谢您的反馈!',
|
||||
style: TextStyle(fontSize: 14, color: _kInkDeep)),
|
||||
@@ -1563,10 +1711,13 @@ class _FeedbackDialogState extends State<_FeedbackDialog> {
|
||||
ElevatedButton(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _kBurgundy, foregroundColor: Colors.white),
|
||||
backgroundColor: _kBurgundy, foregroundColor: Colors.white),
|
||||
child: _submitting
|
||||
? const SizedBox(width: 16, height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('提交'),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -25,8 +26,7 @@ class PublicShopProductsScreen extends StatefulWidget {
|
||||
_PublicShopProductsScreenState();
|
||||
}
|
||||
|
||||
class _PublicShopProductsScreenState
|
||||
extends State<PublicShopProductsScreen> {
|
||||
class _PublicShopProductsScreenState extends State<PublicShopProductsScreen> {
|
||||
final _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
@@ -83,8 +83,8 @@ class _PublicShopProductsScreenState
|
||||
queryParameters: {'page': _page, 'page_size': 20},
|
||||
);
|
||||
final body = resp.data as Map<String, dynamic>;
|
||||
final list = (body['data'] as List<dynamic>? ?? [])
|
||||
.cast<Map<String, dynamic>>();
|
||||
final list =
|
||||
(body['data'] as List<dynamic>? ?? []).cast<Map<String, dynamic>>();
|
||||
final total = (body['total'] as num?)?.toInt() ?? 0;
|
||||
|
||||
if (!mounted) return;
|
||||
@@ -108,8 +108,7 @@ class _PublicShopProductsScreenState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title =
|
||||
widget.shopName.isNotEmpty ? widget.shopName : '本店商品';
|
||||
final title = widget.shopName.isNotEmpty ? widget.shopName : '本店商品';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _kPaperDeep,
|
||||
@@ -118,16 +117,14 @@ class _PublicShopProductsScreenState
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
icon: const Icon(LucideIcons.arrowLeft, size: 18),
|
||||
color: _kInkDeep,
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _kInkDeep),
|
||||
fontSize: 16, fontWeight: FontWeight.w600, color: _kInkDeep),
|
||||
),
|
||||
centerTitle: true,
|
||||
bottom: PreferredSize(
|
||||
@@ -142,8 +139,7 @@ class _PublicShopProductsScreenState
|
||||
Widget _buildBody() {
|
||||
if (_loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: _kBurgundy, strokeWidth: 2));
|
||||
child: CircularProgressIndicator(color: _kBurgundy, strokeWidth: 2));
|
||||
}
|
||||
if (_error != null && _items.isEmpty) {
|
||||
return Center(
|
||||
@@ -157,7 +153,7 @@ class _PublicShopProductsScreenState
|
||||
height: 56,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFDECEC), shape: BoxShape.circle),
|
||||
child: const Icon(Icons.error_outline,
|
||||
child: const Icon(LucideIcons.circleAlert,
|
||||
size: 28, color: Color(0xFFD14343)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -187,10 +183,9 @@ class _PublicShopProductsScreenState
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.inventory_2_outlined, size: 48, color: _kTextMid),
|
||||
Icon(LucideIcons.box, size: 48, color: _kTextMid),
|
||||
SizedBox(height: 12),
|
||||
Text('暂无上架商品',
|
||||
style: TextStyle(fontSize: 15, color: _kTextMid)),
|
||||
Text('暂无上架商品', style: TextStyle(fontSize: 15, color: _kTextMid)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -210,8 +205,7 @@ class _PublicShopProductsScreenState
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'共 $_total 件商品',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: _kTextMid),
|
||||
style: const TextStyle(fontSize: 12, color: _kTextMid),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -219,12 +213,10 @@ class _PublicShopProductsScreenState
|
||||
padding: const EdgeInsets.all(12),
|
||||
sliver: SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) =>
|
||||
_ProductCard(item: _items[index]),
|
||||
(context, index) => _ProductCard(item: _items[index]),
|
||||
childCount: _items.length,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
@@ -247,8 +239,7 @@ class _PublicShopProductsScreenState
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: Text('已展示全部商品',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: _kTextMid))),
|
||||
style: TextStyle(fontSize: 12, color: _kTextMid))),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -275,8 +266,8 @@ class _ProductCard extends StatelessWidget {
|
||||
final qtyStr = qtyNum == qtyNum.roundToDouble()
|
||||
? qtyNum.toInt().toString()
|
||||
: qtyNum.toString();
|
||||
final images = (item['images'] as List<dynamic>? ?? [])
|
||||
.cast<Map<String, dynamic>>();
|
||||
final images =
|
||||
(item['images'] as List<dynamic>? ?? []).cast<Map<String, dynamic>>();
|
||||
final imageUrl = images.isNotEmpty
|
||||
? AppConfig.baseUrl + (images.first['url'] as String)
|
||||
: null;
|
||||
@@ -308,8 +299,7 @@ class _ProductCard extends StatelessWidget {
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
_PlaceholderImg(),
|
||||
errorBuilder: (_, __, ___) => _PlaceholderImg(),
|
||||
)
|
||||
: _PlaceholderImg(),
|
||||
),
|
||||
@@ -317,8 +307,7 @@ class _ProductCard extends StatelessWidget {
|
||||
// 商品信息
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(8, 6, 8, 8),
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -338,8 +327,7 @@ class _ProductCard extends StatelessWidget {
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: _kTextMid),
|
||||
style: const TextStyle(fontSize: 11, color: _kTextMid),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
@@ -391,7 +379,7 @@ class _PlaceholderImg extends StatelessWidget {
|
||||
return Container(
|
||||
color: _kPaperDeep,
|
||||
child: const Center(
|
||||
child: Icon(Icons.wine_bar_outlined, size: 40, color: _kBorder),
|
||||
child: Icon(LucideIcons.wine, size: 40, color: _kBorder),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user