import 'dart:async'; import '../../core/utils/dialog_util.dart'; import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/auth/auth_state.dart'; import '../../core/config/app_config.dart'; import '../../core/config/app_info.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/app_theme.dart'; import '../../models/number_rule.dart'; import '../../models/user.dart'; import '../../providers/license_provider.dart'; import '../../providers/number_rule_provider.dart'; import '../../providers/user_provider.dart'; import '../../providers/shop_provider.dart'; import '../../models/shop.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { // System params local state (UI only, no backend yet) String _sysName = '酒库管理系统'; String _sysCurrency = '人民币(CNY)'; String _sysDateFormat = 'YYYY-MM-DD'; String _sysTimezone = 'Asia/Shanghai (UTC+8)'; bool _requireStockInApproval = true; bool _requireStockOutApproval = true; bool _allowOverstock = false; @override Widget build(BuildContext context) { return DefaultTabController( length: 6, child: Column( children: [ Container( color: AppTheme.surface, child: const TabBar( isScrollable: true, labelColor: AppTheme.primary, unselectedLabelColor: AppTheme.textSecondary, indicatorColor: AppTheme.primary, indicatorWeight: 2, labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), tabs: [ Tab(text: '酒行信息'), Tab(text: '用户管理'), Tab(text: '编号规则'), Tab(text: '系统参数'), Tab(text: '授权'), Tab(text: '数据管理'), ], ), ), const Divider(height: 1), Expanded( child: TabBarView( children: [ _buildShopInfoTab(), _buildUsersTab(), _buildNumberRulesTab(), _buildSystemParamsTab(), _buildLicenseTab(), _buildImportTab(), ], ), ), ], ), ); } Widget _buildShopInfoTab() { final asyncShop = ref.watch(shopInfoProvider); final currentUser = ref.watch(authStateProvider).user; final isAdmin = currentUser?.role == 'admin' || currentUser?.role == 'superadmin'; return asyncShop.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off, size: 40, color: AppTheme.textSecondary), const SizedBox(height: 12), const Text('暂无数据,网络不可用', style: TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.invalidate(shopInfoProvider), child: const Text('重试'), ), ], ), ), data: (shop) => SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('酒行信息', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 4), const Text('门店的基本信息,仅管理员可编辑', style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Logo 预览行 Row( children: [ _ShopLogoPreview(logoUrl: shop.logoUrl, shopName: shop.name, size: 64), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(shop.name.isNotEmpty ? shop.name : '—', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text(shop.code.isNotEmpty ? shop.code : '—', style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)), ], ), ], ), const Divider(height: 24), _ShopInfoRow( label: '门店地址', value: shop.address.isNotEmpty ? shop.address : '—'), const Divider(height: 16), _ShopInfoRow( label: '联系电话', value: shop.phone.isNotEmpty ? shop.phone : '—'), const Divider(height: 16), _ShopInfoRow( label: '负责人', value: shop.managerName.isNotEmpty ? shop.managerName : '—'), ], ), ), ), if (isAdmin) ...[ const SizedBox(height: 16), ElevatedButton.icon( onPressed: () => _showEditShopDialog(context, shop), icon: const Icon(Icons.edit_outlined, size: 16), label: const Text('编辑信息'), ), ], ], ), ), ); } void _showEditShopDialog(BuildContext context, ShopInfo shop) { showAppDialog( context: context, builder: (ctx) => _ShopEditDialog( shop: shop, onSaved: () => ref.invalidate(shopInfoProvider), ), ); } Widget _buildUsersTab() { final asyncUsers = ref.watch(userListProvider); return Column( children: [ Container( height: 52, color: AppTheme.surface, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ ElevatedButton.icon( onPressed: () => _showAddUserDialog(context), icon: const Icon(Icons.person_add, size: 16), label: const Text('新增用户'), ), ], ), ), const Divider(height: 1), Expanded( child: asyncUsers.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off, size: 40, color: AppTheme.textSecondary), const SizedBox(height: 12), const Text('暂无数据,网络不可用', style: const TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.read(userListProvider.notifier).reload(), child: const Text('重试'), ), ], ), ), data: (users) => SingleChildScrollView( child: DataTable( headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)), columns: const [ DataColumn(label: Text('姓名')), DataColumn(label: Text('用户名')), DataColumn(label: Text('角色')), DataColumn(label: Text('状态')), DataColumn(label: Text('操作')), ], rows: users .map((u) => DataRow(cells: [ DataCell(Row( mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 14, backgroundColor: AppTheme.primary.withOpacity(0.15), child: Text( (u.realName ?? u.username) .substring(0, 1), style: const TextStyle( fontSize: 12, color: AppTheme.primary, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), Text(u.realName ?? u.username), ], )), DataCell(Text(u.username, style: const TextStyle( fontFamily: 'monospace', fontSize: 12))), DataCell(_RoleBadge(u.roleLabel)), DataCell(Switch( value: u.isActive, onChanged: (v) => ref .read(userListProvider.notifier) .updateUser(u.id, {'is_active': v}), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, )), DataCell(Row( mainAxisSize: MainAxisSize.min, children: [ TextButton( onPressed: () => _showEditUserDialog(context, u), child: const Text('编辑', style: TextStyle(fontSize: 12)), ), TextButton( onPressed: () => _showResetPasswordDialog(context, u), child: const Text('重置密码', style: TextStyle(fontSize: 12)), ), ], )), ])) .toList(), ), ), ), ), ], ); } // ── 授权 Tab ────────────────────────────────────────────── Widget _buildLicenseTab() { final licenseAsync = ref.watch(licenseProvider); return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('授权信息', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 4), const Text('当前门店的授权状态与到期信息', style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(20), child: licenseAsync.when( loading: () => const _ParamRow(label: '授权状态', value: '加载中…'), error: (_, __) => const _ParamRow(label: '授权状态', value: '暂无授权信息'), data: (lic) { if (lic == null) { return Column( children: [ const _ParamRow(label: '授权状态', value: '未激活'), const SizedBox(height: 12), OutlinedButton.icon( onPressed: _showRenewLicenseDialog, icon: const Icon(Icons.card_membership, size: 16), label: const Text('续费 / 升级授权'), ), ], ); } final statusColor = lic.isExpired ? AppTheme.danger : lic.isActive ? AppTheme.success : AppTheme.textSecondary; final statusText = lic.isExpired ? '已过期' : lic.isActive ? '正常' : '已停用'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _ParamRow(label: '授权类型', value: lic.typeLabel), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ const SizedBox( width: 180, child: Text('授权状态', style: TextStyle( fontSize: 14, color: AppTheme.textSecondary)), ), Text(statusText, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: statusColor)), if (lic.daysRemaining != null && lic.daysRemaining! <= 30) ...[ const SizedBox(width: 8), Chip( label: Text( lic.isExpired ? '已过期' : '剩余 ${lic.daysRemaining} 天', style: const TextStyle( fontSize: 11, color: Colors.white), ), backgroundColor: lic.isExpired ? AppTheme.danger : Colors.orange, padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], ], ), ), if (lic.expiresAt != null) _ParamRow( label: '到期时间', value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!)) else const _ParamRow(label: '到期时间', value: '永久有效'), if (lic.activatedAt != null) _ParamRow( label: '激活时间', value: DateFormat('yyyy-MM-dd').format(lic.activatedAt!)), const SizedBox(height: 12), OutlinedButton.icon( onPressed: _showRenewLicenseDialog, icon: const Icon(Icons.card_membership, size: 16), label: const Text('续费 / 升级授权'), ), ], ); }, ), ), ), ], ), ); } void _showRenewLicenseDialog() { showAppDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('续费 / 升级授权'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('请联系我们获取续费报价:'), const SizedBox(height: 12), SelectableText('📧 ${AppInfo.email}', style: const TextStyle(fontSize: 13)), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('关闭'), ), ElevatedButton( onPressed: () async { await Clipboard.setData(ClipboardData(text: AppInfo.email)); if (ctx.mounted) { Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('邮箱已复制到剪贴板')), ); } }, child: const Text('复制邮箱'), ), ], ), ); } Widget _buildNumberRulesTab() { final asyncRules = ref.watch(numberRuleListProvider); return asyncRules.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off, size: 40, color: AppTheme.textSecondary), const SizedBox(height: 12), const Text('暂无数据,网络不可用', style: const TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.read(numberRuleListProvider.notifier).reload(), child: const Text('重试'), ), ], ), ), data: (rules) => SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('编号规则配置', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 4), const Text('配置各类单据的自动编号规则,修改后对新建单据生效', style: TextStyle( fontSize: 13, color: AppTheme.textSecondary)), const SizedBox(height: 16), Card( child: DataTable( headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)), columns: const [ DataColumn(label: Text('单据类型')), DataColumn(label: Text('前缀')), DataColumn(label: Text('日期格式')), DataColumn(label: Text('下一编号示例')), DataColumn(label: Text('当前序号'), numeric: true), DataColumn(label: Text('操作')), ], rows: rules .map((r) => DataRow(cells: [ DataCell(Text(r.typeLabel, style: const TextStyle( fontWeight: FontWeight.w500))), DataCell(Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppTheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(3), ), child: Text(r.prefix, style: const TextStyle( color: AppTheme.primary, fontFamily: 'monospace', fontSize: 13, fontWeight: FontWeight.w600)), )), DataCell(Text(r.dateFormat, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary))), DataCell(Text(r.exampleNo, style: const TextStyle( fontFamily: 'monospace', fontSize: 12))), DataCell(Text('${r.currentNo}')), DataCell(TextButton( onPressed: () => _showNumberRuleDialog(context, r), child: const Text('编辑', style: TextStyle(fontSize: 12)))), ])) .toList(), ), ), ], ), ), ); } void _showNumberRuleDialog(BuildContext context, NumberRule rule) { final prefixCtrl = TextEditingController(text: rule.prefix); final currentNoCtrl = TextEditingController(text: '${rule.currentNo}'); showAppDialog( context: context, builder: (ctx) => AlertDialog( title: Text('编辑编号规则 — ${rule.typeLabel}'), content: SizedBox( width: context.dialogWidth(360), child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: prefixCtrl, decoration: const InputDecoration(labelText: '前缀'), ), const SizedBox(height: 12), TextField( controller: currentNoCtrl, decoration: const InputDecoration(labelText: '当前序号'), keyboardType: TextInputType.number, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')), ElevatedButton( onPressed: () async { Navigator.of(ctx).pop(); try { await ref .read(numberRuleListProvider.notifier) .updateRule(rule.id, { 'prefix': prefixCtrl.text.trim(), 'current_no': int.tryParse(currentNoCtrl.text) ?? rule.currentNo, }); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('编号规则已更新'), backgroundColor: AppTheme.success), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('更新失败:$e'), backgroundColor: AppTheme.danger), ); } } }, child: const Text('保存'), ), ], ), ); } Widget _buildSystemParamsTab() { return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('系统参数', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('基本设置', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)), const Divider(height: 24), _ParamRow( label: '系统名称', value: _sysName, onEdit: () => _showEditParamDialog('系统名称', _sysName, (v) => setState(() => _sysName = v)), ), _ParamRow( label: '货币单位', value: _sysCurrency, onEdit: () => _showEditParamDialog('货币单位', _sysCurrency, (v) => setState(() => _sysCurrency = v)), ), _ParamRow( label: '日期格式', value: _sysDateFormat, onEdit: () => _showEditParamDialog('日期格式', _sysDateFormat, (v) => setState(() => _sysDateFormat = v)), ), _ParamRow( label: '时区', value: _sysTimezone, onEdit: () => _showEditParamDialog('时区', _sysTimezone, (v) => setState(() => _sysTimezone = v)), ), const SizedBox(height: 16), const Text('审核设置', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)), const Divider(height: 24), _ParamRow( label: '入库单需要审核', value: _requireStockInApproval ? '是' : '否', switchValue: _requireStockInApproval, onSwitchChanged: (v) => setState(() => _requireStockInApproval = v), ), _ParamRow( label: '出库单需要审核', value: _requireStockOutApproval ? '是' : '否', switchValue: _requireStockOutApproval, onSwitchChanged: (v) => setState(() => _requireStockOutApproval = v), ), _ParamRow( label: '允许超量出库', value: _allowOverstock ? '是' : '否', switchValue: _allowOverstock, onSwitchChanged: (v) => setState(() => _allowOverstock = v), ), ], ), ), ), const SizedBox(height: 12), Row( children: [ ElevatedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('设置已保存'), backgroundColor: AppTheme.success)); }, child: const Text('保存设置'), ), const SizedBox(width: 8), OutlinedButton( onPressed: () => setState(() { _sysName = '酒库管理系统'; _sysCurrency = '人民币(CNY)'; _sysDateFormat = 'YYYY-MM-DD'; _sysTimezone = 'Asia/Shanghai (UTC+8)'; _requireStockInApproval = true; _requireStockOutApproval = true; _allowOverstock = false; }), child: const Text('重置默认'), ), ], ), ], ), ); } // ── 数据导入 Tab ────────────────────────────────────────── Widget _buildImportTab() { final currentUser = ref.watch(authStateProvider).user; final isSuperAdmin = currentUser?.role == 'superadmin'; return _BatchImportWidget(isSuperAdmin: isSuperAdmin); } void _showEditParamDialog( String label, String current, ValueChanged onSave) { final ctrl = TextEditingController(text: current); showAppDialog( context: context, builder: (ctx) => AlertDialog( title: Text('修改$label'), content: SizedBox( width: context.dialogWidth(320), child: TextField( controller: ctrl, autofocus: true, decoration: InputDecoration(labelText: label), ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')), ElevatedButton( onPressed: () { final v = ctrl.text.trim(); if (v.isNotEmpty) onSave(v); Navigator.of(ctx).pop(); }, child: const Text('确定'), ), ], ), ); } void _showAddUserDialog(BuildContext context) { showAppDialog( context: context, builder: (ctx) => _UserFormDialog( onSaved: () => ref.read(userListProvider.notifier).reload(), ), ); } void _showEditUserDialog(BuildContext context, AppUser user) { showAppDialog( context: context, builder: (ctx) => _UserFormDialog( user: user, onSaved: () => ref.read(userListProvider.notifier).reload(), ), ); } void _showResetPasswordDialog(BuildContext context, AppUser user) { showAppDialog( context: context, builder: (ctx) => _ResetPasswordDialog(user: user), ); } } // ── 新增/编辑用户弹窗 ───────────────────────────────────── class _UserFormDialog extends ConsumerStatefulWidget { final AppUser? user; final VoidCallback onSaved; const _UserFormDialog({this.user, required this.onSaved}); @override ConsumerState<_UserFormDialog> createState() => _UserFormDialogState(); } class _UserFormDialogState extends ConsumerState<_UserFormDialog> { final _formKey = GlobalKey(); late final TextEditingController _usernameCtrl; late final TextEditingController _realNameCtrl; late final TextEditingController _phoneCtrl; late final TextEditingController _passwordCtrl; late String _role; bool _saving = false; bool get _isEdit => widget.user != null; @override void initState() { super.initState(); _usernameCtrl = TextEditingController(text: widget.user?.username ?? ''); _realNameCtrl = TextEditingController(text: widget.user?.realName ?? ''); _phoneCtrl = TextEditingController(text: widget.user?.phone ?? ''); _passwordCtrl = TextEditingController(); _role = widget.user?.role ?? 'operator'; } @override void dispose() { _usernameCtrl.dispose(); _realNameCtrl.dispose(); _phoneCtrl.dispose(); _passwordCtrl.dispose(); super.dispose(); } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { final notifier = ref.read(userListProvider.notifier); if (_isEdit) { await notifier.updateUser(widget.user!.id, { 'real_name': _realNameCtrl.text.trim(), 'phone': _phoneCtrl.text.trim(), 'role': _role, }); } else { await notifier.createUser({ 'username': _usernameCtrl.text.trim(), 'password': _passwordCtrl.text, 'real_name': _realNameCtrl.text.trim(), 'phone': _phoneCtrl.text.trim(), 'role': _role, }); } if (mounted) { Navigator.of(context).pop(); widget.onSaved(); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(_isEdit ? '用户更新成功' : '用户创建成功'), backgroundColor: AppTheme.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('保存失败:$e'), backgroundColor: AppTheme.danger, )); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: Text(_isEdit ? '编辑用户' : '新增用户'), content: SizedBox( width: context.dialogWidth(400), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (!_isEdit) TextFormField( controller: _usernameCtrl, decoration: const InputDecoration(labelText: '用户名'), validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null, ), if (!_isEdit) const SizedBox(height: 12), TextFormField( controller: _realNameCtrl, decoration: const InputDecoration(labelText: '姓名'), ), const SizedBox(height: 12), TextFormField( controller: _phoneCtrl, decoration: const InputDecoration(labelText: '手机号'), ), const SizedBox(height: 12), if (!_isEdit) ...[ TextFormField( controller: _passwordCtrl, obscureText: true, decoration: const InputDecoration(labelText: '初始密码'), validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null, ), const SizedBox(height: 12), ], DropdownButtonFormField( value: _role, items: const [ DropdownMenuItem(value: 'superadmin', child: Text('超级管理员')), DropdownMenuItem(value: 'admin', child: Text('管理员')), DropdownMenuItem(value: 'operator', child: Text('操作员')), DropdownMenuItem(value: 'readonly', child: Text('只读')), ], onChanged: (v) => setState(() => _role = v ?? 'operator'), decoration: const InputDecoration(labelText: '角色'), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), ElevatedButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Text('保存'), ), ], ); } } // ── 重置密码弹窗 ────────────────────────────────────────── class _ResetPasswordDialog extends ConsumerStatefulWidget { final AppUser user; const _ResetPasswordDialog({required this.user}); @override ConsumerState<_ResetPasswordDialog> createState() => _ResetPasswordDialogState(); } class _ResetPasswordDialogState extends ConsumerState<_ResetPasswordDialog> { final _ctrl = TextEditingController(); bool _saving = false; @override void dispose() { _ctrl.dispose(); super.dispose(); } Future _save() async { if (_ctrl.text.isEmpty) return; setState(() => _saving = true); try { await ref.read(userListProvider.notifier).resetPassword(widget.user.id, _ctrl.text); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('密码已重置'), backgroundColor: AppTheme.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('重置失败:$e'), backgroundColor: AppTheme.danger, )); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: Text('重置密码 — ${widget.user.username}'), content: SizedBox( width: context.dialogWidth(360), child: TextField( controller: _ctrl, obscureText: true, decoration: const InputDecoration(labelText: '新密码'), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), ElevatedButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Text('确认重置'), ), ], ); } } class _RoleBadge extends StatelessWidget { final String role; const _RoleBadge(this.role); @override Widget build(BuildContext context) { final Color bg; final Color fg; switch (role) { case '超级管理员': bg = const Color(0xFFFCE4EC); fg = AppTheme.danger; break; case '管理员': bg = const Color(0xFFE3F2FD); fg = AppTheme.primary; break; case '操作员': bg = const Color(0xFFE8F5E9); fg = AppTheme.success; break; default: bg = const Color(0xFFF5F5F5); fg = AppTheme.textSecondary; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(3)), child: Text(role, style: TextStyle(color: fg, fontSize: 12, fontWeight: FontWeight.w500)), ); } } class _ParamRow extends StatelessWidget { final String label; final String value; final bool? switchValue; final ValueChanged? onSwitchChanged; final VoidCallback? onEdit; const _ParamRow({ required this.label, required this.value, this.switchValue, this.onSwitchChanged, this.onEdit, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ SizedBox( width: 180, child: Text(label, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary)), ), if (switchValue != null) Switch( value: switchValue!, onChanged: onSwitchChanged, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ) else Text(value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500)), const Spacer(), if (onEdit != null) TextButton( onPressed: onEdit, child: const Text('修改', style: TextStyle(fontSize: 12)), ), ], ), ); } } // ── 批量数据导入 ─────────────────────────────────────────── class _ImportSlot { final String title; final String endpoint; final String hint; PlatformFile? file; // null = 未运行;true = 成功;false = 失败 bool? success; String? error; int total = 0; int imported = 0; int skipped = 0; int updated = 0; // 进度 int uploadPercent = 0; bool isProcessing = false; int processingSeconds = 0; _ImportSlot(this.title, this.endpoint, this.hint); bool get hasResult => success != null; void resetProgress() { uploadPercent = 0; isProcessing = false; processingSeconds = 0; success = null; error = null; } } class _BatchImportWidget extends ConsumerStatefulWidget { final bool isSuperAdmin; const _BatchImportWidget({required this.isSuperAdmin}); @override ConsumerState<_BatchImportWidget> createState() => _BatchImportWidgetState(); } class _BatchImportWidgetState extends ConsumerState<_BatchImportWidget> { bool _loading = false; String? _lastDir; OverlayEntry? _importBarrier; static const _prefKey = 'import_last_dir'; late final List<_ImportSlot> _slots = [ _ImportSlot('往来单位', '/import/partners', '格式:编号 | 类型 | 状态 | 名称 | 电话 | 卡号 | 初始金额 | 单位 | 地址 | 备注'), _ImportSlot('商品名称', '/import/product-names', '格式:选项编号 | 选项名称 | 备注'), _ImportSlot('商品系列', '/import/product-series', '格式:选项编号 | 选项名称 | 备注'), _ImportSlot('商品规格', '/import/product-specs', '格式:选项编号 | 选项名称 | 单品数量 | 备注'), _ImportSlot('库存', '/import/inventory', '格式:商品编号|商品名称|系列|规格|单位|库存数量|单价|金额|生产日期|批次|分类|所在仓库|入库日期|供应商|上次盘点|备注'), ]; final Set _selectedTables = {}; bool _clearing = false; static const _clearOptions = [ (key: 'stock_in', label: '入库单'), (key: 'stock_out', label: '出库单'), (key: 'inventory', label: '库存管理'), (key: 'products', label: '商品详情'), (key: 'partners', label: '往来单位'), ]; @override void initState() { super.initState(); SharedPreferences.getInstance().then((prefs) { final dir = prefs.getString(_prefKey); if (dir != null && mounted) setState(() => _lastDir = dir); }); } Future _pickFile(int index) async { try { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['xls', 'xlsx'], withData: true, initialDirectory: kIsWeb ? null : _lastDir, ); if (result == null || result.files.isEmpty) return; // 保存目录供下次使用(仅非 web 平台) if (!kIsWeb) { final path = result.files.first.path; if (path != null) { final dir = path.contains('/') ? path.substring(0, path.lastIndexOf('/')) : null; if (dir != null) { _lastDir = dir; SharedPreferences.getInstance().then((p) => p.setString(_prefKey, dir)); } } } if (!mounted) return; setState(() { _slots[index].file = result.files.first; _slots[index].success = null; _slots[index].error = null; }); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('选择文件失败:$e')), ); } } void _showBarrier() { _importBarrier = OverlayEntry( builder: (_) => Stack(children: [ const ModalBarrier(dismissible: false, color: Colors.transparent), Positioned( bottom: 24, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8), ), child: const Row(mainAxisSize: MainAxisSize.min, children: [ SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)), SizedBox(width: 10), Text('导入中,请勿切换页面…', style: TextStyle(color: Colors.white, fontSize: 13)), ]), ), ), ), ]), ); Overlay.of(context, rootOverlay: true).insert(_importBarrier!); } void _removeBarrier() { _importBarrier?.remove(); _importBarrier = null; } Future _runImport() async { final token = ref.read(authStateProvider).user?.accessToken ?? ''; if (!_slots.any((s) => s.file != null)) return; _showBarrier(); setState(() { _loading = true; for (final s in _slots) { if (s.file != null) s.resetProgress(); } }); final dio = Dio(BaseOptions( baseUrl: AppConfig.apiBaseUrl, sendTimeout: const Duration(seconds: 120), receiveTimeout: const Duration(seconds: 300), )); dio.options.headers['Authorization'] = 'Bearer $token'; for (final slot in _slots) { if (slot.file == null) continue; final bytes = slot.file!.bytes; if (bytes == null) { if (mounted) setState(() { slot.success = false; slot.error = '无法读取文件'; }); continue; } Timer? processingTimer; bool uploadDone = false; try { final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes(bytes, filename: slot.file!.name), }); final resp = await dio.post( slot.endpoint, data: formData, onSendProgress: (sent, total) { if (total <= 0 || !mounted) return; if (sent >= total && !uploadDone) { uploadDone = true; setState(() { slot.isProcessing = true; slot.processingSeconds = 0; }); processingTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (!mounted) return; setState(() => slot.processingSeconds++); }); } else if (!uploadDone) { final pct = (sent / total * 100).round().clamp(0, 99); if (mounted) setState(() => slot.uploadPercent = pct); } }, ); processingTimer?.cancel(); final data = (resp.data is Map) ? resp.data as Map : {}; if (mounted) setState(() { slot.success = true; slot.imported = (data['imported'] ?? 0) as int; slot.skipped = (data['skipped'] ?? 0) as int; slot.updated = (data['updated'] ?? 0) as int; slot.total = (data['total'] ?? slot.imported + slot.skipped + slot.updated) as int; slot.isProcessing = false; }); } on DioException catch (e) { processingTimer?.cancel(); final raw = e.response?.data; final msg = (raw is Map ? raw['error'] : null) ?? e.message ?? '未知错误'; if (mounted) setState(() { slot.success = false; slot.error = msg.toString(); slot.isProcessing = false; }); } catch (e) { processingTimer?.cancel(); if (mounted) setState(() { slot.success = false; slot.error = e.toString(); slot.isProcessing = false; }); } } if (mounted) { _removeBarrier(); setState(() => _loading = false); } } @override Widget build(BuildContext context) { final hasAnyFile = _slots.any((s) => s.file != null); final ran = _slots.where((s) => s.file != null && s.hasResult).toList(); final allDone = !_loading && ran.length == _slots.where((s) => s.file != null).length && ran.isNotEmpty; final failCount = ran.where((s) => s.success == false).length; return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('数据导入', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 4), const Text( '选择对应的 Excel 文件后点击「全部导入」,系统将依次导入,按名称去重(已存在的数据不重复导入)。', style: TextStyle(fontSize: 13, color: AppTheme.textSecondary), ), const SizedBox(height: 20), Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Column( children: [ for (int i = 0; i < _slots.length; i++) ...[ _buildSlotRow(i), if (i < _slots.length - 1) const Divider(height: 1), ], ], ), ), ), const SizedBox(height: 16), Row( children: [ ElevatedButton.icon( onPressed: (_loading || !hasAnyFile) ? null : _runImport, icon: _loading ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Icon(Icons.upload_rounded, size: 16), label: Text(_loading ? '导入中...' : '全部导入'), ), const SizedBox(width: 16), if (allDone) ...[ Icon( failCount == 0 ? Icons.check_circle_outline : Icons.warning_amber_rounded, size: 16, color: failCount == 0 ? AppTheme.success : Colors.orange, ), const SizedBox(width: 4), Text( failCount == 0 ? '全部导入完成' : '$failCount 项失败,请检查错误信息', style: TextStyle( fontSize: 13, color: failCount == 0 ? AppTheme.success : Colors.orange, ), ), ], ], ), if (widget.isSuperAdmin) ...[ const SizedBox(height: 32), const Divider(), const SizedBox(height: 16), _buildClearDataSection(), ], ], ), ); } Widget _buildClearDataSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题行 Row( children: [ const Icon(Icons.delete_forever, color: AppTheme.danger, size: 20), const SizedBox(width: 8), const Text('危险操作 — 数据清空', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.danger)), ], ), const SizedBox(height: 12), // 红色警告框 Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFFFF3F3), border: Border.all(color: AppTheme.danger.withOpacity(0.5)), borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Icon(Icons.warning_amber_rounded, color: AppTheme.danger, size: 18), const SizedBox(width: 6), const Text('警告:数据一旦清空,无法恢复!', style: TextStyle( color: AppTheme.danger, fontWeight: FontWeight.w600, fontSize: 13)), ]), const SizedBox(height: 6), const Text( '• 清空操作仅删除当前门店的数据,不影响其他门店\n' '• 操作不可撤销,请务必提前备份数据\n' '• 仅超级管理员可执行此操作', style: TextStyle(fontSize: 12, color: Color(0xFFB71C1C), height: 1.6), ), ], ), ), const SizedBox(height: 14), // 勾选框 Wrap( spacing: 8, runSpacing: 4, children: _clearOptions.map((opt) { final selected = _selectedTables.contains(opt.key); return FilterChip( label: Text(opt.label), selected: selected, selectedColor: AppTheme.danger.withOpacity(0.15), checkmarkColor: AppTheme.danger, labelStyle: TextStyle( color: selected ? AppTheme.danger : AppTheme.textPrimary, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, ), side: BorderSide( color: selected ? AppTheme.danger : Colors.grey.shade300, ), onSelected: _clearing ? null : (v) => setState(() { if (v) { _selectedTables.add(opt.key); } else { _selectedTables.remove(opt.key); } }), ); }).toList(), ), const SizedBox(height: 14), // 清空按钮 ElevatedButton.icon( onPressed: (_clearing || _selectedTables.isEmpty) ? null : () => _confirmClear(), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.danger, foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey.shade300, ), icon: _clearing ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.delete_sweep_outlined, size: 18), label: Text(_clearing ? '清空中...' : '清空选中数据'), ), ], ); } Future _confirmClear() async { final tables = List.from(_selectedTables); final labels = _clearOptions .where((o) => tables.contains(o.key)) .map((o) => o.label) .toList(); final ctrl = TextEditingController(); final confirmed = await showDialog( context: context, barrierDismissible: false, builder: (ctx) => StatefulBuilder( builder: (ctx, setS) => AlertDialog( title: Row(children: [ const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 22), const SizedBox(width: 8), const Text('确认清空数据', style: TextStyle(color: Colors.red)), ]), content: SizedBox( width: context.dialogWidth(400), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('即将清空以下数据表:', style: TextStyle(fontWeight: FontWeight.w500)), const SizedBox(height: 8), ...labels.map((l) => Padding( padding: const EdgeInsets.only(left: 12, bottom: 4), child: Row(children: [ const Icon(Icons.remove_circle, color: Colors.red, size: 14), const SizedBox(width: 6), Text(l, style: const TextStyle( color: Colors.red, fontWeight: FontWeight.w600)), ]), )), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFFFF3F3), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.red.shade200), ), child: const Text( '⚠️ 此操作不可撤销,数据清空后无法恢复!', style: TextStyle( color: Colors.red, fontSize: 13, fontWeight: FontWeight.w600), ), ), const SizedBox(height: 16), const Text('请输入 "确认清空" 以继续:', style: TextStyle(fontSize: 13)), const SizedBox(height: 8), TextField( controller: ctrl, autofocus: true, decoration: const InputDecoration( hintText: '确认清空', border: OutlineInputBorder(), isDense: true, ), onChanged: (_) => setS(() {}), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消'), ), ElevatedButton( onPressed: ctrl.text == '确认清空' ? () => Navigator.of(ctx).pop(true) : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('确认清空'), ), ], ), ), ); ctrl.dispose(); if (confirmed != true || !mounted) return; setState(() => _clearing = true); try { final token = ref.read(authStateProvider).user?.accessToken ?? ''; final dio = Dio(BaseOptions(baseUrl: AppConfig.apiBaseUrl)); dio.options.headers['Authorization'] = 'Bearer $token'; await dio.post('/admin/clear-data', data: {'tables': tables}); if (!mounted) return; setState(() => _selectedTables.clear()); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('已清空:${labels.join('、')}'), backgroundColor: AppTheme.success, ), ); } on DioException catch (e) { if (!mounted) return; final msg = (e.response?.data is Map ? e.response!.data['error'] : null) ?? e.message ?? '未知错误'; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('清空失败:$msg'), backgroundColor: AppTheme.danger), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('清空失败:$e'), backgroundColor: AppTheme.danger), ); } finally { if (mounted) setState(() => _clearing = false); } } Widget _buildSlotRow(int index) { final slot = _slots[index]; Widget? statusWidget; if (slot.hasResult) { if (slot.success == true) { final duplicate = slot.skipped + slot.updated; final parts = [ '共 ${slot.total} 条', '新增 ${slot.imported} 条', if (duplicate > 0) '重复 $duplicate 条', ]; statusWidget = Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check_circle_outline, size: 15, color: AppTheme.success), const SizedBox(width: 4), Text( parts.join(','), style: const TextStyle(fontSize: 13, color: AppTheme.success), ), ], ); } else { statusWidget = Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 15, color: AppTheme.danger), const SizedBox(width: 4), Text( slot.error ?? '未知错误', style: const TextStyle(fontSize: 13, color: AppTheme.danger), ), ], ); } } else if (_loading && slot.file != null) { if (slot.isProcessing) { statusWidget = Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primary), ), const SizedBox(width: 6), Text( '导入数据${slot.processingSeconds > 0 ? "(${slot.processingSeconds}秒)" : ""}', style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary), ), ], ); } else { statusWidget = Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 80, child: LinearProgressIndicator( value: slot.uploadPercent / 100, backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation(AppTheme.primary), minHeight: 6, ), ), const SizedBox(width: 6), Text( '上传 ${slot.uploadPercent}%', style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary), ), ], ); } } return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ SizedBox( width: 68, child: Text(slot.title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), ), const SizedBox(width: 12), OutlinedButton( onPressed: _loading ? null : () => _pickFile(index), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: const Text('选择文件', style: TextStyle(fontSize: 13)), ), const SizedBox(width: 12), Expanded( child: Text( slot.file?.name ?? '未选择', style: TextStyle( fontSize: 13, color: slot.file != null ? AppTheme.textPrimary : AppTheme.textSecondary, ), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 12), if (statusWidget != null) statusWidget, ], ), const SizedBox(height: 3), Padding( padding: const EdgeInsets.only(left: 80), child: Text( slot.hint, style: const TextStyle(fontSize: 11, color: AppTheme.textSecondary), ), ), ], ), ); } } // ── 门店 Logo 预览 ──────────────────────────────────────── class _ShopLogoPreview extends StatelessWidget { final String logoUrl; final String shopName; final double size; const _ShopLogoPreview({required this.logoUrl, required this.shopName, this.size = 64}); @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: radius, ), alignment: Alignment.center, child: Text( initial, style: TextStyle( color: Colors.white, fontSize: size * 0.5, fontWeight: FontWeight.w600, ), ), ); } } // ── 酒行信息行 ──────────────────────────────────────────── class _ShopInfoRow extends StatelessWidget { final String label; final String value; const _ShopInfoRow({required this.label, required this.value}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ SizedBox( width: 88, child: Text(label, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary)), ), Expanded( child: Text(value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500)), ), ], ), ); } } // ── 编辑酒行信息弹窗 ───────────────────────────────────── class _ShopEditDialog extends ConsumerStatefulWidget { final ShopInfo shop; final VoidCallback onSaved; const _ShopEditDialog({required this.shop, required this.onSaved}); @override ConsumerState<_ShopEditDialog> createState() => _ShopEditDialogState(); } class _ShopEditDialogState extends ConsumerState<_ShopEditDialog> { late final TextEditingController _nameCtrl; late final TextEditingController _addressCtrl; late final TextEditingController _phoneCtrl; late final TextEditingController _managerCtrl; late final TextEditingController _wechatCtrl; bool _saving = false; bool _uploadingLogo = false; @override void initState() { super.initState(); _nameCtrl = TextEditingController(text: widget.shop.name); _addressCtrl = TextEditingController(text: widget.shop.address); _phoneCtrl = TextEditingController(text: widget.shop.phone); _managerCtrl = TextEditingController(text: widget.shop.managerName); _wechatCtrl = TextEditingController(text: widget.shop.wechatId); } @override void dispose() { _nameCtrl.dispose(); _addressCtrl.dispose(); _phoneCtrl.dispose(); _managerCtrl.dispose(); _wechatCtrl.dispose(); super.dispose(); } Future _uploadLogo() async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['jpg', 'jpeg', 'png'], withData: true, ); if (result == null || result.files.isEmpty) return; final file = result.files.first; if (file.bytes == null) return; setState(() => _uploadingLogo = true); try { await ref.read(shopRepositoryProvider).uploadLogo(file.bytes!, file.name); ref.invalidate(shopInfoProvider); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Logo 已更新'), backgroundColor: AppTheme.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('上传失败:$e'), backgroundColor: AppTheme.danger, )); } } finally { if (mounted) setState(() => _uploadingLogo = false); } } Future _save() async { setState(() => _saving = true); try { await ref.read(shopRepositoryProvider).updateInfo({ 'name': _nameCtrl.text.trim(), 'address': _addressCtrl.text.trim(), 'phone': _phoneCtrl.text.trim(), 'manager_name': _managerCtrl.text.trim(), 'wechat_id': _wechatCtrl.text.trim(), }); if (mounted) { Navigator.of(context).pop(); widget.onSaved(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('信息已更新'), backgroundColor: AppTheme.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('保存失败:$e'), backgroundColor: AppTheme.danger, )); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('编辑酒行信息'), content: SizedBox( width: context.dialogWidth(400), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Logo 上传行 Row( children: [ _ShopLogoPreview( logoUrl: ref.watch(shopInfoProvider).valueOrNull?.logoUrl ?? widget.shop.logoUrl, shopName: _nameCtrl.text, size: 56, ), const SizedBox(width: 12), OutlinedButton.icon( onPressed: _uploadingLogo ? null : _uploadLogo, icon: _uploadingLogo ? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.upload_outlined, size: 16), label: const Text('更换 Logo'), ), ], ), const SizedBox(height: 16), TextField( controller: _nameCtrl, decoration: const InputDecoration(labelText: '门店名称'), ), const SizedBox(height: 12), TextField( controller: _addressCtrl, decoration: const InputDecoration(labelText: '门店地址'), ), const SizedBox(height: 12), TextField( controller: _phoneCtrl, decoration: const InputDecoration(labelText: '联系电话'), ), const SizedBox(height: 12), TextField( controller: _managerCtrl, decoration: const InputDecoration(labelText: '负责人'), ), const SizedBox(height: 12), TextField( controller: _wechatCtrl, decoration: const InputDecoration( labelText: '微信号(可选)', hintText: '填写后顾客扫码可查看', ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), ElevatedButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Text('保存'), ), ], ); } }