import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import '../../core/config/app_config.dart'; import '../../core/theme/app_theme.dart'; class PublicProductScreen extends StatefulWidget { final String publicId; const PublicProductScreen({super.key, required this.publicId}); @override State createState() => _PublicProductScreenState(); } class _PublicProductScreenState extends State { Map? _data; String? _error; bool _loading = true; final _dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 15), )); @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _loading = true; _error = null; }); try { final resp = await _dio.get( '${AppConfig.apiBaseUrl}/public/products/${widget.publicId}', ); final data = (resp.data as Map)['data'] as Map; debugPrint('[public] description=${data['description']}'); setState(() { _data = data; _loading = false; }); } catch (e) { setState(() { _error = e.toString(); _loading = false; }); } } @override Widget build(BuildContext context) { if (_loading) { return const Scaffold( backgroundColor: Color(0xFFF5F5F5), body: Center(child: CircularProgressIndicator()), ); } if (_error != null || _data == null) { return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: AppTheme.danger), const SizedBox(height: 16), const Text('商品不存在或已下架', style: TextStyle(fontSize: 16, color: Color(0xFF333333))), const SizedBox(height: 16), ElevatedButton(onPressed: _load, child: const Text('重试')), ], ), ), ); } final d = _data!; final images = (d['images'] as List? ?? []).cast>(); 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? ?? ''; final brand = d['brand'] as String? ?? ''; final unit = d['unit'] as String? ?? ''; final description = d['description'] as String? ?? ''; return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: SafeArea( child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: _ImageGallery(imageUrls: imageUrls), ), const SliverToBoxAdapter(child: SizedBox(height: 8)), SliverToBoxAdapter( child: _TitleCard(name: name, series: series, spec: spec, brand: brand), ), const SliverToBoxAdapter(child: SizedBox(height: 8)), SliverToBoxAdapter( child: _ParamsCard(spec: spec, brand: brand, unit: unit, description: description), ), const SliverFillRemaining( hasScrollBody: false, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [_FooterBrand()], ), ), ], ), ), ); } } // ── 图片画廊 ───────────────────────────────────────────── class _ImageGallery extends StatefulWidget { final List imageUrls; const _ImageGallery({required this.imageUrls}); @override State<_ImageGallery> createState() => _ImageGalleryState(); } class _ImageGalleryState extends State<_ImageGallery> { int _current = 0; final PageController _ctrl = PageController(); @override void dispose() { _ctrl.dispose(); super.dispose(); } void _openFullscreen(int index) { Navigator.of(context).push(PageRouteBuilder( opaque: false, barrierColor: Colors.black87, pageBuilder: (_, __, ___) => _FullscreenViewer( urls: widget.imageUrls, initialIndex: index, ), )); } @override Widget build(BuildContext context) { final urls = widget.imageUrls; return LayoutBuilder(builder: (context, constraints) { final size = constraints.maxWidth; if (urls.isEmpty) { return Container( width: size, height: size, color: const Color(0xFFEEEEEE), child: const Center( child: Icon(Icons.wine_bar, size: 96, color: Color(0xFFCCCCCC)), ), ); } return Column( children: [ // 主图区(正方形) SizedBox( width: size, height: size, child: Stack( children: [ PageView.builder( controller: _ctrl, itemCount: urls.length, onPageChanged: (i) => setState(() => _current = i), itemBuilder: (_, i) => GestureDetector( onTap: () => _openFullscreen(i), child: Image.network( urls[i], fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, __, ___) => Container( color: const Color(0xFFEEEEEE), child: const Center( child: Icon(Icons.broken_image, size: 64, color: Color(0xFFCCCCCC)), ), ), ), ), ), if (urls.length > 1) Positioned( right: 12, bottom: 12, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(12), ), child: Text( '${_current + 1} / ${urls.length}', style: const TextStyle(fontSize: 12, color: Colors.white), ), ), ), Positioned( right: 12, top: 12, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: Colors.black38, borderRadius: BorderRadius.circular(4), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.zoom_in, size: 14, color: Colors.white), SizedBox(width: 3), Text('点击放大', style: TextStyle(fontSize: 11, color: Colors.white)), ], ), ), ), ], ), ), // 缩略图条 if (urls.length > 1) Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: SizedBox( height: 60, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: urls.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) { final selected = i == _current; return GestureDetector( onTap: () => _ctrl.animateToPage(i, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 60, height: 60, decoration: BoxDecoration( border: Border.all( color: selected ? AppTheme.primary : const Color(0xFFDDDDDD), width: selected ? 2 : 1, ), borderRadius: BorderRadius.circular(4), ), child: ClipRRect( borderRadius: BorderRadius.circular(3), child: Image.network( urls[i], fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon( Icons.broken_image, size: 24, color: Color(0xFFCCCCCC), ), ), ), ), ); }, ), ), ), ], ); }); } } // ── 全屏查看器 ──────────────────────────────────────────── class _FullscreenViewer extends StatefulWidget { final List urls; final int initialIndex; const _FullscreenViewer({required this.urls, required this.initialIndex}); @override State<_FullscreenViewer> createState() => _FullscreenViewerState(); } class _FullscreenViewerState extends State<_FullscreenViewer> { late int _current; late PageController _ctrl; @override void initState() { super.initState(); _current = widget.initialIndex; _ctrl = PageController(initialPage: widget.initialIndex); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Scaffold( backgroundColor: Colors.transparent, body: Stack( children: [ PageView.builder( controller: _ctrl, itemCount: widget.urls.length, onPageChanged: (i) => setState(() => _current = i), itemBuilder: (_, i) => Center( child: InteractiveViewer( child: Image.network( widget.urls[i], fit: BoxFit.contain, errorBuilder: (_, __, ___) => const Icon( Icons.broken_image, size: 64, color: Colors.white54, ), ), ), ), ), Positioned( top: 40, right: 16, child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), ), child: const Icon(Icons.close, color: Colors.white, size: 20), ), ), ), if (widget.urls.length > 1) Positioned( bottom: 40, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.urls.length, (i) => AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.symmetric(horizontal: 3), width: i == _current ? 16 : 6, height: 6, decoration: BoxDecoration( color: i == _current ? Colors.white : Colors.white38, borderRadius: BorderRadius.circular(3), ), )), ), ), ], ), ), ); } } // ── 标题卡 ──────────────────────────────────────────────── class _TitleCard extends StatelessWidget { final String name, series, spec, brand; const _TitleCard({required this.name, required this.series, required this.spec, required this.brand}); @override Widget build(BuildContext context) { final subtitle = [if (series.isNotEmpty) series, if (spec.isNotEmpty) spec].join(' · '); return Container( color: Colors.white, padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF212121), height: 1.4)), if (subtitle.isNotEmpty) ...[ const SizedBox(height: 6), Text(subtitle, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), ], if (brand.isNotEmpty) ...[ const SizedBox(height: 10), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFE8F0FE), borderRadius: BorderRadius.circular(4), ), child: Text('品牌:$brand', style: const TextStyle(fontSize: 12, color: AppTheme.primary, fontWeight: FontWeight.w500)), ), ], ], ), ); } } // ── 商品参数卡 ──────────────────────────────────────────── class _ParamsCard extends StatelessWidget { final String spec, brand, unit, description; const _ParamsCard({required this.spec, required this.brand, required this.unit, required this.description}); @override Widget build(BuildContext context) { final rows = <({String label, String value})>[]; if (spec.isNotEmpty) rows.add((label: '规格', value: spec)); if (brand.isNotEmpty) rows.add((label: '品牌', value: brand)); if (unit.isNotEmpty) rows.add((label: '单位', value: unit)); if (description.isNotEmpty) rows.add((label: '描述', value: description)); if (rows.isEmpty) return const SizedBox.shrink(); return Container( color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.fromLTRB(16, 14, 16, 10), child: Text('商品参数', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF212121))), ), const Divider(height: 1, color: Color(0xFFF0F0F0)), ...rows.map((r) => Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11), child: Row( children: [ SizedBox( width: 72, child: Text(r.label, style: const TextStyle(fontSize: 13, color: Color(0xFF888888))), ), Expanded( child: Text(r.value, style: const TextStyle(fontSize: 13, color: Color(0xFF212121))), ), ], ), ), if (r != rows.last) const Divider(height: 1, indent: 16, color: Color(0xFFF5F5F5)), ], )), const SizedBox(height: 4), ], ), ); } } // ── 页脚 ────────────────────────────────────────────────── class _FooterBrand extends StatelessWidget { const _FooterBrand(); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 20), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.wine_bar, size: 14, color: Color(0xFFBBBBBB)), SizedBox(width: 6), Text('酒库管理系统提供', style: TextStyle(fontSize: 12, color: Color(0xFFBBBBBB))), ], ), ); } }