import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'dart:async'; import '../../core/auth/auth_state.dart'; import '../../core/theme/app_theme.dart'; import '../../providers/connectivity_provider.dart'; import '../../providers/update_provider.dart'; class AppShell extends ConsumerStatefulWidget { final Widget child; const AppShell({super.key, required this.child}); @override ConsumerState createState() => _AppShellState(); } class _AppShellState extends ConsumerState { bool _sidebarExpanded = true; final String _loginTime = DateFormat('HH:mm:ss').format(DateTime.now()); bool _forceDialogShown = false; void _showForceUpdateDialog( BuildContext context, AppUpdateInfo info) { if (_forceDialogShown) return; _forceDialogShown = true; showDialog( context: context, barrierDismissible: false, builder: (ctx) => PopScope( canPop: false, child: AlertDialog( title: const Row( children: [ Icon(Icons.system_update, color: AppTheme.primary), SizedBox(width: 8), Text('发现新版本'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('当前版本需要更新至 v${info.latestVersion} 才能继续使用。'), if (info.releaseNotes.isNotEmpty) ...[ const SizedBox(height: 12), Text(info.releaseNotes, style: const TextStyle( color: AppTheme.textSecondary, fontSize: 13)), ], ], ), actions: [ ElevatedButton( onPressed: () => launchUpdateUrl(info.downloadUrls), child: const Text('立即更新'), ), ], ), ), ); } final List<_NavItem> _navItems = const [ _NavItem(icon: Icons.input, label: '入库管理', path: '/stock-in'), _NavItem(icon: Icons.output, label: '出库管理', path: '/stock-out'), _NavItem(icon: Icons.inventory_2, label: '库存管理', path: '/inventory'), _NavItem(icon: Icons.track_changes, label: '商品管理', path: '/batches'), _NavItem( icon: Icons.account_balance_wallet, label: '财务管理', path: '/finance'), _NavItem(icon: Icons.people, label: '往来单位', path: '/partners'), _NavItem(icon: Icons.category, label: '基础数据', path: '/products'), _NavItem(icon: Icons.settings, label: '系统设置', path: '/settings'), ]; @override Widget build(BuildContext context) { final user = ref.watch(authStateProvider).user; final isOnline = ref.watch(connectivityProvider); final location = GoRouterState.of(context).matchedLocation; final sidebarWidth = _sidebarExpanded ? 200.0 : 56.0; final updateNotifier = ref.watch(updateProvider.notifier); final updateInfo = ref.watch(updateProvider).valueOrNull; final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0'; return Scaffold( body: Column( children: [ // Top Bar Container( height: 56, color: AppTheme.primary, padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( children: [ IconButton( icon: Icon( _sidebarExpanded ? Icons.menu_open : Icons.menu, color: Colors.white), onPressed: () => setState(() => _sidebarExpanded = !_sidebarExpanded), tooltip: _sidebarExpanded ? '收起侧边栏' : '展开侧边栏', ), const SizedBox(width: 4), _ShopButton(user: user, version: appVersion), const Spacer(), if (user != null) ...[ MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () => _showShopPanel(context, user, version: appVersion), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.business, color: Colors.white70, size: 14), const SizedBox(width: 4), Text(user.shopNo, style: const TextStyle( color: Colors.white70, fontSize: 13)), ], ), ), ), const SizedBox(width: 20), const Icon(Icons.person_outline, color: Colors.white70, size: 14), const SizedBox(width: 4), Text(user.username, style: const TextStyle( color: Colors.white70, fontSize: 13)), const SizedBox(width: 8), PopupMenuButton( icon: const Icon(Icons.keyboard_arrow_down, color: Colors.white70), offset: const Offset(0, 36), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), color: Colors.white, elevation: 8, onSelected: (v) { if (v == 'logout') { ref.read(authStateProvider.notifier).logout(); context.go('/login'); } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'profile', padding: EdgeInsets.zero, child: _HoverMenuItem( icon: Icons.manage_accounts_outlined, label: '个人设置', ), ), const PopupMenuDivider(height: 1), const PopupMenuItem( value: 'logout', padding: EdgeInsets.zero, child: _HoverMenuItem( icon: Icons.logout_outlined, label: '退出登录', danger: true, ), ), ], ), const SizedBox(width: 8), ], ], ), ), // Main area Expanded( child: Row( children: [ // Sidebar AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, width: sidebarWidth, color: AppTheme.primaryDark, child: Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), children: _navItems.map((item) { final isActive = location.startsWith(item.path); return _SidebarItem( item: item, isActive: isActive, expanded: _sidebarExpanded, onTap: () => context.go(item.path), ); }).toList(), ), ), ], ), ), // Content area Expanded( child: Column( children: [ // Update banner(非强制更新) if (updateInfo != null && updateInfo.hasUpdate && !updateInfo.forceUpdate && !updateNotifier.isDismissed) Container( width: double.infinity, color: const Color(0xFFFFF8E1), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 6), child: Row( children: [ const Icon(Icons.system_update, size: 16, color: Color(0xFFF57F17)), const SizedBox(width: 8), Expanded( child: Text( '发现新版本 v${updateInfo.latestVersion}' '${updateInfo.releaseNotes.isNotEmpty ? " · ${updateInfo.releaseNotes}" : ""}', style: const TextStyle( color: Color(0xFF5D4037), fontSize: 13), overflow: TextOverflow.ellipsis, ), ), TextButton( onPressed: () => launchUpdateUrl( updateInfo.downloadUrls), style: TextButton.styleFrom( foregroundColor: const Color(0xFFF57F17)), child: const Text('立即更新'), ), TextButton( onPressed: updateNotifier.dismiss, style: TextButton.styleFrom( foregroundColor: const Color(0xFF9E9E9E)), child: const Text('稍后再说'), ), ], ), ), // 强制更新 dialog(用 postFrameCallback 避免 build 中 showDialog) if (updateInfo != null && updateInfo.hasUpdate && updateInfo.forceUpdate) Builder(builder: (ctx) { WidgetsBinding.instance.addPostFrameCallback((_) { _showForceUpdateDialog(ctx, updateInfo); }); return const SizedBox.shrink(); }), // Offline banner if (!isOnline) Container( width: double.infinity, color: AppTheme.danger, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 6), child: const Row( children: [ Icon(Icons.wifi_off, size: 16, color: Colors.white), SizedBox(width: 8), Text( '网络连接已断开 · 当前处于只读模式,所有写操作已禁用', style: TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500), ), ], ), ), Expanded(child: widget.child), // Status bar LayoutBuilder( builder: (context, constraints) { final w = constraints.maxWidth; // Three tiers: wide / medium / narrow final wide = w >= 650; final medium = w >= 190; final iconOnly = !medium; return Container( height: 28, color: isOnline ? const Color(0xFF37474F) : AppTheme.danger, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ if (!isOnline) ...[ const Icon(Icons.wifi_off, size: 11, color: Colors.white70), if (!iconOnly) ...[ const SizedBox(width: 4), const Text('离线', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600)), ], const _StatusDivider(), ], if (isOnline && user != null) ...[ _StatusItem( icon: Icons.store, text: user.shopNo, iconOnly: iconOnly), const _StatusDivider(), _StatusItem( icon: Icons.person, text: user.username, iconOnly: iconOnly), if (wide) ...[ const _StatusDivider(), _StatusItem( icon: Icons.login, text: '登录时间:$_loginTime'), const _StatusDivider(), const _ClockWidget(), ] else const _StatusDivider(), ], const Spacer(), _StatusItem( icon: isOnline ? Icons.cloud_done_outlined : Icons.cloud_off_outlined, text: isOnline ? '已连接' : '连接已断开', iconOnly: iconOnly, ), const _StatusDivider(), _StatusItem( icon: Icons.info_outline, text: ref .watch(appVersionProvider) .valueOrNull ?? 'v1.0.0', iconOnly: iconOnly), ], ), ); }, ), ], ), ), ], ), ), ], ), ); } } class _NavItem { final IconData icon; final String label; final String path; const _NavItem( {required this.icon, required this.label, required this.path}); } class _SidebarItem extends StatelessWidget { final _NavItem item; final bool isActive; final bool expanded; final VoidCallback onTap; const _SidebarItem({ required this.item, required this.isActive, required this.expanded, required this.onTap, }); @override Widget build(BuildContext context) { return SizedBox( height: 48, child: Material( color: isActive ? AppTheme.primary.withAlpha(76) : Colors.transparent, child: InkWell( onTap: onTap, hoverColor: Colors.white.withAlpha(13), splashColor: Colors.white.withAlpha(26), highlightColor: Colors.white.withAlpha(13), child: Row( children: [ // Active indicator bar Container( width: 3, height: isActive ? 28 : 0, color: Colors.white, margin: EdgeInsets.only(right: expanded ? 13 : 0), ), if (!isActive) SizedBox(width: expanded ? 16 : 3), Expanded( child: Row( mainAxisAlignment: expanded ? MainAxisAlignment.start : MainAxisAlignment.center, children: [ Icon( item.icon, color: isActive ? Colors.white : Colors.white60, size: 20, ), if (expanded) ...[ const SizedBox(width: 12), Flexible( child: Text( item.label, style: TextStyle( color: isActive ? Colors.white : Colors.white70, fontSize: 14, fontWeight: isActive ? FontWeight.w500 : FontWeight.normal, ), overflow: TextOverflow.ellipsis, ), ), ], ], ), ), ], ), ), ), ); } } class _StatusItem extends StatelessWidget { final IconData icon; final String text; final bool iconOnly; const _StatusItem( {required this.icon, required this.text, this.iconOnly = false}); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 11, color: Colors.white54), if (!iconOnly) ...[ const SizedBox(width: 4), Text(text, style: const TextStyle(color: Colors.white54, fontSize: 11)), ], ], ); } } class _StatusDivider extends StatelessWidget { const _StatusDivider(); @override Widget build(BuildContext context) { return Container( width: 1, height: 12, color: Colors.white24, margin: const EdgeInsets.symmetric(horizontal: 10), ); } } class _ClockWidget extends StatefulWidget { const _ClockWidget(); @override State<_ClockWidget> createState() => _ClockWidgetState(); } class _ClockWidgetState extends State<_ClockWidget> { late Timer _timer; late String _time; @override void initState() { super.initState(); _time = DateFormat('HH:mm:ss').format(DateTime.now()); _timer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted) setState(() => _time = DateFormat('HH:mm:ss').format(DateTime.now())); }); } @override void dispose() { _timer.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return _StatusItem(icon: Icons.access_time, text: '当前时间:$_time'); } } void _showShopPanel(BuildContext context, AuthUser u, {String version = 'v1.0.0'}) { showDialog( context: context, builder: (ctx) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: SizedBox( width: 360, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), decoration: const BoxDecoration( color: AppTheme.primary, borderRadius: BorderRadius.vertical(top: Radius.circular(10)), ), child: Row( children: [ const Icon(Icons.store, color: Colors.white, size: 20), const SizedBox(width: 10), const Text('门店信息', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), const Spacer(), IconButton( onPressed: () => Navigator.pop(ctx), icon: const Icon(Icons.close, color: Colors.white70, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), // Info rows Padding( padding: const EdgeInsets.all(20), child: Column( children: [ _InfoRow(icon: Icons.tag, label: '门店编号', value: u.shopNo), const SizedBox(height: 14), _InfoRow(icon: Icons.person, label: '登录账号', value: u.username), const SizedBox(height: 14), _InfoRow(icon: Icons.badge_outlined, label: '姓名', value: u.realName), const SizedBox(height: 14), _InfoRow(icon: Icons.info_outline, label: '系统版本', value: version), ], ), ), ], ), ), ), ); } class _ShopButton extends StatelessWidget { final AuthUser? user; final String version; const _ShopButton({this.user, this.version = 'v1.0.0'}); @override Widget build(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { if (user != null) _showShopPanel(context, user!, version: version); }, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.wine_bar, color: Colors.white, size: 22), SizedBox(width: 8), Text( '酒库管理系统', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: 0.5), ), ], ), ), ); } } class _InfoRow extends StatelessWidget { final IconData icon; final String label; final String value; const _InfoRow({required this.icon, required this.label, required this.value}); @override Widget build(BuildContext context) { return Row( children: [ Icon(icon, size: 16, color: AppTheme.textSecondary), const SizedBox(width: 10), SizedBox( width: 72, child: Text(label, style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary)), ), Expanded( child: Text(value, style: const TextStyle( fontSize: 13, color: AppTheme.textPrimary, fontWeight: FontWeight.w500)), ), ], ); } } class _HoverMenuItem extends StatefulWidget { final IconData icon; final String label; final bool danger; const _HoverMenuItem({ required this.icon, required this.label, this.danger = false, }); @override State<_HoverMenuItem> createState() => _HoverMenuItemState(); } class _HoverMenuItemState extends State<_HoverMenuItem> { bool _hovered = false; @override Widget build(BuildContext context) { final color = widget.danger ? const Color(0xFFE53935) : const Color(0xFF333333); final hoverBg = widget.danger ? const Color(0xFFFFF0F0) : const Color(0xFFF0F4FF); return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), cursor: SystemMouseCursors.click, child: AnimatedContainer( duration: const Duration(milliseconds: 120), width: 152, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _hovered ? hoverBg : Colors.transparent, borderRadius: BorderRadius.circular(4), ), child: Row( children: [ Icon(widget.icon, size: 16, color: color), const SizedBox(width: 10), Text( widget.label, style: TextStyle( fontSize: 14, color: color, fontWeight: FontWeight.w400, ), ), ], ), ), ); } }