import 'dart:async'; import '../../core/utils/dialog_util.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:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/auth/auth_state.dart'; import '../../widgets/write_guard.dart'; import '../../core/config/app_config.dart'; import '../../core/config/app_info.dart'; import '../../core/config/license_copy.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/theme/app_tokens.dart'; import '../../core/theme/app_tokens.g.dart'; import '../../core/theme/theme_controller.dart'; import '../../widgets/kpi_card.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 { /// 初始选中的 Tab(0=酒行信息 … 4=授权 … 5=数据管理)。 final int initialTab; const SettingsScreen({super.key, this.initialTab = 0}); @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: 5, initialIndex: widget.initialTab >= 5 ? 0 : widget.initialTab, child: Column( children: [ Container( color: context.tokens.surface, child: TabBar( isScrollable: true, labelColor: context.tokens.primary, unselectedLabelColor: context.tokens.muted, indicatorColor: context.tokens.primary, indicatorWeight: 2, labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), tabs: const [ Tab(text: '酒行信息'), Tab(text: '用户管理'), Tab(text: '编号规则'), Tab(text: '系统参数'), Tab(text: '授权'), ], ), ), const Divider(height: 1), Expanded( child: TabBarView( children: [ _buildShopInfoTab(), _buildUsersTab(), _buildNumberRulesTab(), _buildSystemParamsTab(), _buildLicenseTab(), ], ), ), ], ), ); } 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: [ Icon(Icons.cloud_off, size: 40, color: context.tokens.muted), const SizedBox(height: 12), Text('暂无数据,网络不可用', style: TextStyle(color: context.tokens.muted)), 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), Text('门店的基本信息,仅管理员可编辑', style: TextStyle(fontSize: 13, color: context.tokens.muted)), 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: TextStyle(fontSize: 13, color: context.tokens.muted)), ], ), ], ), 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 : '—'), const Divider(height: 16), _ShopInfoRow( label: '微信号', value: shop.wechatId.isNotEmpty ? shop.wechatId : '—'), ], ), ), ), 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: context.tokens.surface, padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ WriteGuard( child: 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: [ Icon(Icons.cloud_off, size: 40, color: context.tokens.muted), const SizedBox(height: 12), Text('暂无数据,网络不可用', style: TextStyle(color: context.tokens.muted)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.read(userListProvider.notifier).reload(), child: const Text('重试'), ), ], ), ), data: (users) => SingleChildScrollView( child: DataTable( headingRowColor: WidgetStateProperty.all(context.tokens.thBg), 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: context.tokens.primary.withOpacity(0.15), child: Text( (u.realName ?? u.username) .substring(0, 1), style: TextStyle( fontSize: 12, color: context.tokens.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: WriteGuard.isReadonly(ref) ? const [] : [ WriteGuard( child: TextButton( onPressed: () => _showEditUserDialog(context, u), child: const Text('编辑', style: TextStyle(fontSize: 12)), ), ), WriteGuard( child: 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), Text('当前门店的授权状态与设备绑定', style: TextStyle(fontSize: 13, color: context.tokens.muted)), 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) => _buildLicenseContent(lic), ), ), ), const SizedBox(height: 16), // 过期降级说明 _buildExpiryNotes(), const SizedBox(height: 16), // 激活码输入区 _buildActivationCard(), ], ), ); } Widget _buildLicenseContent(LicenseInfo? lic) { if (lic == null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, 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 Color phaseColor = LicenseCopy.phaseColor(lic.phase); final String phaseText = LicenseCopy.statusText(lic); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _ParamRow(label: '授权类型', value: lic.typeLabel), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ SizedBox( width: 180, child: Text('授权状态', style: TextStyle( fontSize: 14, color: context.tokens.muted)), ), Text(phaseText, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: phaseColor)), ], ), ), if (lic.expiresAt != null) _ParamRow( label: '到期时间', value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!)) else const _ParamRow(label: '到期时间', value: '永久有效'), _ParamRow(label: '设备上限', value: '${lic.maxDevices} 台'), const SizedBox(height: 12), OutlinedButton.icon( onPressed: _showRenewLicenseDialog, icon: const Icon(Icons.card_membership, size: 16), label: const Text('续费 / 升级授权'), ), ], ); } Widget _buildExpiryNotes() { return Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.info_outline, size: 16, color: context.tokens.muted), const SizedBox(width: 6), const Text('过期说明', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 4), Text('授权到期后会分阶段降级,请在到期前及时续费', style: TextStyle(fontSize: 13, color: context.tokens.muted)), const SizedBox(height: 12), for (final note in LicenseCopy.degradationNotes()) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 6, right: 8), child: Icon(Icons.circle, size: 5, color: context.tokens.muted), ), Expanded( child: Text(note, style: const TextStyle( fontSize: 13, height: 1.5)), ), ], ), ), ], ), ), ); } Widget _buildActivationCard() { return Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('兑换激活码', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), const SizedBox(height: 8), Text('输入购买或活动获得的激活码,时长将叠加到当前授权之后', style: TextStyle( fontSize: 13, color: context.tokens.muted)), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _licenseKeyCtrl, decoration: const InputDecoration( hintText: 'JIUKU-XXXX-XXXX', isDense: true, border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), style: const TextStyle(fontSize: 13), ), ), if (!WriteGuard.isReadonly(ref)) ...[ const SizedBox(width: 8), ElevatedButton( onPressed: _isActivating ? null : _activateLicense, child: _isActivating ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('兑换'), ), ], ], ), ], ), ), ); } late final TextEditingController _licenseKeyCtrl = TextEditingController(); bool _isActivating = false; Future _activateLicense() async { final key = _licenseKeyCtrl.text.trim(); if (key.isEmpty) return; setState(() => _isActivating = true); try { final repo = ref.read(licenseRepositoryProvider); await repo.activate(key); _licenseKeyCtrl.clear(); await ref.read(licenseProvider.notifier).reload(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('兑换成功,授权已更新'), backgroundColor: context.tokens.success), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), backgroundColor: context.tokens.danger), ); } } finally { if (mounted) setState(() => _isActivating = false); } } 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: [ Icon(Icons.cloud_off, size: 40, color: context.tokens.muted), const SizedBox(height: 12), Text('暂无数据,网络不可用', style: TextStyle(color: context.tokens.muted)), 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), Text('配置各类单据的自动编号规则,修改后对新建单据生效', style: TextStyle( fontSize: 13, color: context.tokens.muted)), const SizedBox(height: 16), Card( child: DataTable( headingRowColor: WidgetStateProperty.all(context.tokens.thBg), 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: context.tokens.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(3), ), child: Text(r.prefix, style: TextStyle( color: context.tokens.primary, fontFamily: 'monospace', fontSize: 13, fontWeight: FontWeight.w600)), )), DataCell(Text(r.dateFormat, style: TextStyle( fontSize: 12, color: context.tokens.muted))), DataCell(Text(r.exampleNo, style: const TextStyle( fontFamily: 'monospace', fontSize: 12))), DataCell(Text('${r.currentNo}')), DataCell(WriteGuard( child: 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( SnackBar( content: const Text('编号规则已更新'), backgroundColor: context.tokens.success), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('更新失败:$e'), backgroundColor: context.tokens.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), const _ThemePickerCard(), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('基本设置', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: context.tokens.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), Text('审核设置', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: context.tokens.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(SnackBar( content: const Text('设置已保存'), backgroundColor: context.tokens.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('重置默认'), ), ], ), ], ), ); } 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: context.tokens.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('保存失败:$e'), backgroundColor: context.tokens.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(SnackBar( content: const Text('密码已重置'), backgroundColor: context.tokens.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('重置失败:$e'), backgroundColor: context.tokens.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 t = context.tokens; final (Color fg, Color bg) = switch (role) { '超级管理员' => (t.danger, t.dangerBg), '管理员' => (t.primary, t.infoSoft), '操作员' => (t.success, t.okSoft), _ => (t.muted, t.infoSoft), }; return StatusPill(label: role, color: fg, background: bg); } } 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: TextStyle( fontSize: 14, color: context.tokens.muted)), ), 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 _ThemePickerCard extends ConsumerWidget { const _ThemePickerCard(); // (key, 名称, 说明) static const List<(String, String, String)> _themes = [ ('a', '经典蓝', '浅色 · 默认'), ('b', '琥珀', '深色'), ('c', '酒窖', '暖色'), ]; @override Widget build(BuildContext context, WidgetRef ref) { final current = ref.watch(themeControllerProvider); return Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('外观主题', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: context.tokens.primaryDark)), const SizedBox(height: 4), Text('切换配色方案,立即生效并记住选择', style: TextStyle(fontSize: 13, color: context.tokens.muted)), const SizedBox(height: 16), Wrap( spacing: 12, runSpacing: 12, children: [ for (final (key, name, desc) in _themes) _ThemeSwatch( preview: appTokensOf(key), name: name, desc: desc, selected: current == key, onTap: () => ref.read(themeControllerProvider.notifier).set(key), ), ], ), ], ), ), ); } } class _ThemeSwatch extends StatelessWidget { final AppTokens preview; final String name; final String desc; final bool selected; final VoidCallback onTap; const _ThemeSwatch({ required this.preview, required this.name, required this.desc, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(10), onTap: onTap, child: Container( width: 150, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: context.tokens.surface, border: Border.all( color: selected ? context.tokens.primary : context.tokens.border, width: selected ? 2 : 1, ), borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 微缩预览:用该主题真实 token 渲染一小块 chrome Container( height: 46, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: preview.page, borderRadius: BorderRadius.circular(6), border: Border.all(color: preview.border), ), child: Row( children: [ Container( width: 22, height: 34, decoration: BoxDecoration( color: preview.surface, borderRadius: BorderRadius.circular(3), border: Border.all(color: preview.border), ), ), const SizedBox(width: 6), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 36, height: 6, decoration: BoxDecoration( color: preview.primary, borderRadius: BorderRadius.circular(3))), const SizedBox(height: 5), Container( width: 24, height: 5, decoration: BoxDecoration( color: preview.muted, borderRadius: BorderRadius.circular(3))), ], ), ), ], ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600)), Text(desc, style: TextStyle( fontSize: 11, color: context.tokens.muted)), ], ), ), if (selected) Icon(Icons.check_circle, size: 18, color: context.tokens.primary), ], ), ], ), ), ); } } // ── 门店 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: TextStyle( fontSize: 14, color: context.tokens.muted)), ), 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(SnackBar( content: const Text('Logo 已更新'), backgroundColor: context.tokens.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('上传失败:$e'), backgroundColor: context.tokens.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(SnackBar( content: const Text('信息已更新'), backgroundColor: context.tokens.success, )); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('保存失败:$e'), backgroundColor: context.tokens.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('保存'), ), ], ); } }