import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_colors.dart'; import '../../core/i18n/app_localizations.dart'; import '../shell/nav_bar_config.dart'; /// 权限管理 - 页面3.2 【管理员专属】 class AdminPermissionsPage extends ConsumerStatefulWidget { const AdminPermissionsPage({super.key}); @override ConsumerState createState() => _AdminPermissionsPageState(); } class _AdminPermissionsPageState extends ConsumerState { final _searchCtrl = TextEditingController(); Timer? _debounce; String _searchQuery = ''; // 模拟当前登录用户(自保护用) static const _currentUserId = '0001'; final _employees = <_Employee>[ _Employee( name: '张三', avatarText: '张', employeeId: '0048', department: '销售部', roles: ['普通员工', '审批人'], isActive: true, ), _Employee( name: '王经理', avatarText: '王', employeeId: '0012', department: '销售部', roles: ['审批人', '系统管理员'], isActive: true, ), _Employee( name: '李会计', avatarText: '李', employeeId: '0025', department: '财务部', roles: ['财务人员'], isActive: true, ), _Employee( name: '赵管理员', avatarText: '赵', employeeId: '0001', department: '信息技术部', roles: ['系统管理员'], isActive: true, ), _Employee( name: '钱六', avatarText: '钱', employeeId: '0052', department: '财务部', roles: ['财务人员'], isActive: false, ), _Employee( name: '孙七', avatarText: '孙', employeeId: '0078', department: '行政部', roles: ['普通员工'], isActive: true, ), _Employee( name: '周八', avatarText: '周', employeeId: '0091', department: '技术部', roles: ['普通员工'], isActive: true, ), ]; List<_Employee> get _filteredEmployees { if (_searchQuery.isEmpty) return _employees; final q = _searchQuery.toLowerCase(); return _employees.where((e) { return e.name.toLowerCase().contains(q) || e.employeeId.toLowerCase().contains(q); }).toList(); } @override void initState() { super.initState(); } @override void dispose() { _searchCtrl.dispose(); _debounce?.cancel(); super.dispose(); } void _onSearchChanged(String value) { _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 300), () { if (mounted) { setState(() => _searchQuery = value); } }); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('permissionManagement'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ _buildSearchBar(), Expanded( child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _filteredEmployees.length, itemBuilder: (_, i) => _buildEmpCard(_filteredEmployees[i]), ), ), ], ); } // ── 搜索栏(300ms 防抖) ── Widget _buildSearchBar() { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 10, 16, 10), color: AppColors.bgCard, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(18), border: Border.all(color: AppColors.border), ), child: Row( children: [ const Icon(Icons.search, size: 18, color: AppColors.textPlaceholder), const SizedBox(width: 6), Expanded( child: TextField( controller: _searchCtrl, onChanged: _onSearchChanged, decoration: const InputDecoration( hintText: '输入姓名或工号进行检索...', hintStyle: TextStyle(fontSize: 14, color: AppColors.textPlaceholder), border: InputBorder.none, contentPadding: EdgeInsets.symmetric(vertical: 10), isDense: true, ), style: const TextStyle(fontSize: 14, color: AppColors.textPrimary), ), ), if (_searchCtrl.text.isNotEmpty) GestureDetector( onTap: () { _searchCtrl.clear(); _onSearchChanged(''); setState(() => _searchQuery = ''); }, child: const Icon(Icons.clear, size: 16, color: AppColors.textPlaceholder), ), ], ), ), ); } // ── 员工卡片 ── Widget _buildEmpCard(_Employee emp) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: GestureDetector( onTap: () => _openPermissionDrawer(emp), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: emp.isActive ? AppColors.bgCard : const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(8), boxShadow: const [ BoxShadow( color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 1)), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头像 Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(20), ), child: Center( child: Text( emp.avatarText, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), const SizedBox(width: 10), // 信息区 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( emp.name, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(width: 6), Text( '工号:${emp.employeeId}', style: const TextStyle( fontSize: 12, color: AppColors.textPlaceholder, ), ), ], ), const SizedBox(height: 2), Text( emp.department, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ), // 角色标签区 Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ ...emp.roles.map( (role) => Padding( padding: const EdgeInsets.only(bottom: 4), child: _buildRoleTag(role), ), ), if (!emp.isActive) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.dangerBg, borderRadius: BorderRadius.circular(3), ), child: const Text('已禁用', style: TextStyle(fontSize: 10, color: AppColors.danger)), ), ], ), ], ), ), ), ); } Widget _buildRoleTag(String role) { Color bgColor; Color textColor; switch (role) { case '审批人': bgColor = AppColors.warningBg; textColor = AppColors.warning; break; case '财务人员': bgColor = AppColors.successBg; textColor = AppColors.success; break; case '系统管理员': bgColor = AppColors.dangerBg; textColor = AppColors.danger; break; default: bgColor = AppColors.primaryLight; textColor = AppColors.primary; } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(3), ), child: Text(role, style: TextStyle(fontSize: 10, color: textColor)), ); } // ── 权限抽屉(右侧滑出) ── void _openPermissionDrawer(_Employee emp) { // 深拷贝当前权限状态 final checked = {}; for (final perm in _allPermissions) { checked[perm.id] = _getDefaultPerms(emp.roles).contains(perm.id); } showGeneralDialog( context: context, barrierDismissible: true, barrierLabel: '', barrierColor: Colors.black45, transitionDuration: const Duration(milliseconds: 300), pageBuilder: (ctx, anim1, anim2) { return _PermissionDrawer( employee: emp, checked: checked, currentUserId: _currentUserId, onSave: () { Navigator.of(ctx).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('权限已更新'), duration: Duration(seconds: 2)), ); }, onCancel: () => Navigator.of(ctx).pop(), ); }, transitionBuilder: (ctx, anim, secondaryAnim, child) { return SlideTransition( position: Tween( begin: const Offset(1.0, 0.0), end: Offset.zero, ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)), child: child, ); }, ); } } // ── 权限数据定义 ── class _PermModule { final String name; final List<_PermItem> items; const _PermModule({required this.name, required this.items}); } class _PermItem { final String id; final String label; const _PermItem({required this.id, required this.label}); } const _allPermissions = <_PermItem>[ _PermItem(id: 'expense.apply', label: '发起报销'), _PermItem(id: 'expense.view_own', label: '查看本人报销'), _PermItem(id: 'expense.view_dept', label: '查看部门报销'), _PermItem(id: 'expense.view_all', label: '查看全公司报销'), _PermItem(id: 'expense.approve', label: '审批报销'), _PermItem(id: 'expense.mark_paid', label: '确认付款'), _PermItem(id: 'expense.export', label: '导出报销数据'), _PermItem(id: 'preapply.apply', label: '发起事前申请'), _PermItem(id: 'preapply.view_own', label: '查看本人申请'), _PermItem(id: 'preapply.view_dept', label: '查看部门申请'), _PermItem(id: 'preapply.approve', label: '审批事前申请'), _PermItem(id: 'overtime.apply', label: '发起加班'), _PermItem(id: 'overtime.view_own', label: '查看本人加班'), _PermItem(id: 'overtime.view_dept', label: '查看部门加班'), _PermItem(id: 'overtime.approve', label: '审批加班'), _PermItem(id: 'vehicle.apply', label: '发起用车'), _PermItem(id: 'vehicle.view_own', label: '查看本人用车'), _PermItem(id: 'vehicle.view_dept', label: '查看部门用车'), _PermItem(id: 'vehicle.approve', label: '审批用车'), _PermItem(id: 'outing.create', label: '创建外勤日志'), _PermItem(id: 'outing.view_own', label: '查看本人日志'), _PermItem(id: 'outing.view_dept', label: '查看部门日志'), _PermItem(id: 'outing.comment', label: '点评外勤日志'), _PermItem(id: 'announcement.view', label: '查看公告'), _PermItem(id: 'announcement.create', label: '发布公告'), _PermItem(id: 'report.view', label: '查看报表'), _PermItem(id: 'report.export', label: '导出报表'), _PermItem(id: 'admin.permissions', label: '管理权限'), ]; // 按模块分组 const _permModules = <_PermModule>[ _PermModule(name: '报销管理', items: [ _PermItem(id: 'expense.apply', label: '发起报销'), _PermItem(id: 'expense.view_own', label: '查看本人报销'), _PermItem(id: 'expense.view_dept', label: '查看部门报销'), _PermItem(id: 'expense.view_all', label: '查看全公司报销'), _PermItem(id: 'expense.approve', label: '审批报销'), _PermItem(id: 'expense.mark_paid', label: '确认付款'), _PermItem(id: 'expense.export', label: '导出报销数据'), ]), _PermModule(name: '事前申请', items: [ _PermItem(id: 'preapply.apply', label: '发起事前申请'), _PermItem(id: 'preapply.view_own', label: '查看本人申请'), _PermItem(id: 'preapply.view_dept', label: '查看部门申请'), _PermItem(id: 'preapply.approve', label: '审批事前申请'), ]), _PermModule(name: '加班管理', items: [ _PermItem(id: 'overtime.apply', label: '发起加班'), _PermItem(id: 'overtime.view_own', label: '查看本人加班'), _PermItem(id: 'overtime.view_dept', label: '查看部门加班'), _PermItem(id: 'overtime.approve', label: '审批加班'), ]), _PermModule(name: '用车管理', items: [ _PermItem(id: 'vehicle.apply', label: '发起用车'), _PermItem(id: 'vehicle.view_own', label: '查看本人用车'), _PermItem(id: 'vehicle.view_dept', label: '查看部门用车'), _PermItem(id: 'vehicle.approve', label: '审批用车'), ]), _PermModule(name: '外勤管理', items: [ _PermItem(id: 'outing.create', label: '创建外勤日志'), _PermItem(id: 'outing.view_own', label: '查看本人日志'), _PermItem(id: 'outing.view_dept', label: '查看部门日志'), _PermItem(id: 'outing.comment', label: '点评外勤日志'), ]), _PermModule(name: '公告管理', items: [ _PermItem(id: 'announcement.view', label: '查看公告'), _PermItem(id: 'announcement.create', label: '发布公告'), ]), _PermModule(name: '报表管理', items: [ _PermItem(id: 'report.view', label: '查看报表'), _PermItem(id: 'report.export', label: '导出报表'), ]), _PermModule(name: '系统管理', items: [ _PermItem(id: 'admin.permissions', label: '管理权限'), ]), ]; // 角色预设 const _presets = <_RolePreset>[ _RolePreset(name: '员工', permissions: [ 'expense.apply', 'expense.view_own', 'preapply.apply', 'preapply.view_own', 'overtime.apply', 'overtime.view_own', 'vehicle.apply', 'vehicle.view_own', 'outing.create', 'outing.view_own', 'announcement.view', 'report.view', ]), _RolePreset(name: '审批人', permissions: [ 'expense.apply', 'expense.view_own', 'expense.view_dept', 'expense.approve', 'preapply.apply', 'preapply.view_own', 'preapply.view_dept', 'preapply.approve', 'overtime.apply', 'overtime.view_own', 'overtime.view_dept', 'overtime.approve', 'vehicle.apply', 'vehicle.view_own', 'vehicle.view_dept', 'vehicle.approve', 'outing.create', 'outing.view_own', 'outing.view_dept', 'outing.comment', 'announcement.view', 'report.view', ]), _RolePreset(name: '财务人员', permissions: [ 'expense.apply', 'expense.view_own', 'expense.view_all', 'expense.mark_paid', 'expense.export', 'preapply.apply', 'preapply.view_own', 'announcement.view', 'report.view', 'report.export', ]), _RolePreset(name: '系统管理员', permissions: [ 'expense.apply', 'expense.view_own', 'expense.view_dept', 'expense.view_all', 'expense.approve', 'expense.mark_paid', 'expense.export', 'preapply.apply', 'preapply.view_own', 'preapply.view_dept', 'preapply.approve', 'overtime.apply', 'overtime.view_own', 'overtime.view_dept', 'overtime.approve', 'vehicle.apply', 'vehicle.view_own', 'vehicle.view_dept', 'vehicle.approve', 'outing.create', 'outing.view_own', 'outing.view_dept', 'outing.comment', 'announcement.view', 'announcement.create', 'report.view', 'report.export', 'admin.permissions', ]), ]; Set _getDefaultPerms(List roles) { if (roles.contains('系统管理员')) return _presets[3].permissions.toSet(); if (roles.contains('财务人员')) return _presets[2].permissions.toSet(); if (roles.contains('审批人')) return _presets[1].permissions.toSet(); return _presets[0].permissions.toSet(); } class _RolePreset { final String name; final List permissions; const _RolePreset({required this.name, required this.permissions}); } // ── 员工数据模型 ── class _Employee { final String name; final String avatarText; final String employeeId; final String department; final List roles; final bool isActive; const _Employee({ required this.name, required this.avatarText, required this.employeeId, required this.department, required this.roles, this.isActive = true, }); } // ── 权限抽屉组件 ── class _PermissionDrawer extends StatefulWidget { final _Employee employee; final Map checked; final String currentUserId; final VoidCallback onSave; final VoidCallback onCancel; const _PermissionDrawer({ required this.employee, required this.checked, required this.currentUserId, required this.onSave, required this.onCancel, }); @override State<_PermissionDrawer> createState() => _PermissionDrawerState(); } class _PermissionDrawerState extends State<_PermissionDrawer> { late Map _checked; bool _showHistory = false; bool get _isSelfAdmin => widget.employee.employeeId == widget.currentUserId; // 模拟变更记录 final _mockHistory = [ _ChangeLog( time: '2026-06-04 14:32', operator: '赵管理员', summary: '添加了财务人员角色', ), _ChangeLog( time: '2026-06-03 09:15', operator: '赵管理员', summary: '添加了审批人权限(报销审批、加班审批)', ), _ChangeLog( time: '2026-05-28 16:40', operator: '王经理', summary: '修改为普通员工权限', ), _ChangeLog( time: '2026-05-20 11:00', operator: '赵管理员', summary: '初始权限分配', ), ]; @override void initState() { super.initState(); _checked = Map.from(widget.checked); } @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width * 0.82; return Material( color: Colors.transparent, child: Align( alignment: Alignment.centerRight, child: Container( width: width, height: double.infinity, decoration: const BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.only( topLeft: Radius.circular(12), bottomLeft: Radius.circular(12), ), ), child: Column( children: [ // 标题栏 _buildHeader(), // 可滚动内容 Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildEmployeeInfo(), _buildQuickPresets(), _buildPermissionList(), _buildHistorySection(), const SizedBox(height: 24), ], ), ), ), // 底部保存按钮 _buildSaveButton(), ], ), ), ), ); } Widget _buildHeader() { final l10n = AppLocalizations.of(context); return Container( padding: const EdgeInsets.fromLTRB(16, 48, 8, 12), decoration: const BoxDecoration( color: AppColors.bgCard, border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( children: [ Text(l10n.get('permissionEdit'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const Spacer(), IconButton( icon: const Icon(Icons.close, size: 20, color: AppColors.textSecondary), onPressed: widget.onCancel, ), ], ), ); } Widget _buildEmployeeInfo() { return Container( width: double.infinity, padding: const EdgeInsets.all(16), color: AppColors.bgCard, child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(22), ), child: Center( child: Text( widget.employee.avatarText, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.employee.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const SizedBox(height: 2), Text('${widget.employee.department} · 工号:${widget.employee.employeeId}', style: const TextStyle( fontSize: 12, color: AppColors.textSecondary)), ], ), ), ], ), ); } // ── 快捷套餐 ── Widget _buildQuickPresets() { final l10n = AppLocalizations.of(context); return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), color: AppColors.bgCard, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.get('quickPresets'), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary)), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: _presets.map((preset) { return GestureDetector( onTap: () => _applyPreset(preset), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppColors.primaryLight, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.primary.withValues(alpha: 0.3)), ), child: Text(preset.name, style: const TextStyle( fontSize: 13, color: AppColors.primary, fontWeight: FontWeight.w500)), ), ); }).toList(), ), ], ), ); } void _applyPreset(_RolePreset preset) { if (_isSelfAdmin && preset.name != '系统管理员') { // 自保护:自己是admin不能取消自己的admin final hadAdmin = widget.checked.keys.any( (k) => k == 'admin.permissions' && widget.checked[k] == true); if (hadAdmin && !preset.permissions.contains('admin.permissions')) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('无法取消自己的管理员权限'), duration: Duration(seconds: 2)), ); return; } } setState(() { // 先全重置 for (final k in _checked.keys) { _checked[k] = false; } // 再勾选预设 for (final p in preset.permissions) { if (_checked.containsKey(p)) { _checked[p] = true; } } }); } // ── 权限点列表 ── Widget _buildPermissionList() { final l10n = AppLocalizations.of(context); return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), color: AppColors.bgCard, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.get('permissionItems'), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary)), const SizedBox(height: 8), ..._permModules.map((module) => _buildModuleGroup(module)), ], ), ); } Widget _buildModuleGroup(_PermModule module) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(module.name, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const SizedBox(height: 4), ...module.items.map((perm) { final val = _checked[perm.id] ?? false; final isAdminPerm = perm.id == 'admin.permissions'; final canToggle = !(_isSelfAdmin && isAdminPerm); return GestureDetector( onTap: canToggle ? () => setState(() => _checked[perm.id] = !val) : null, child: Container( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4), child: Row( children: [ Icon( val ? Icons.check_box : Icons.check_box_outline_blank, size: 20, color: val ? canToggle ? AppColors.primary : AppColors.textPlaceholder : AppColors.textPlaceholder, ), const SizedBox(width: 8), Text( perm.label, style: TextStyle( fontSize: 13, color: canToggle ? AppColors.textPrimary : AppColors.textPlaceholder, ), ), if (!canToggle) const Padding( padding: EdgeInsets.only(left: 6), child: Icon(Icons.lock_outline, size: 12, color: AppColors.textPlaceholder), ), ], ), ), ); }), ], ), ); } // ── 变更记录 ── Widget _buildHistorySection() { final l10n = AppLocalizations.of(context); return Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ GestureDetector( onTap: () => setState(() => _showHistory = !_showHistory), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ const Icon(Icons.history, size: 16, color: AppColors.textSecondary), const SizedBox(width: 6), Text(l10n.get('changeLog'), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary)), const Spacer(), Text(l10n.getString('recentItems', args: {'count': '${_mockHistory.length}'}), style: const TextStyle( fontSize: 11, color: AppColors.textPlaceholder)), Icon( _showHistory ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 18, color: AppColors.textPlaceholder, ), ], ), ), ), if (_showHistory) ..._mockHistory.map((log) => _buildTimelineItem(log)), ], ), ); } Widget _buildTimelineItem(_ChangeLog log) { return Container( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: AppColors.primary, shape: BoxShape.circle, ), ), Container( width: 1, height: 40, color: AppColors.border, ), ], ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(log.summary, style: const TextStyle( fontSize: 13, color: AppColors.textPrimary)), const SizedBox(height: 2), Text('${log.operator} · ${log.time}', style: const TextStyle( fontSize: 11, color: AppColors.textPlaceholder)), ], ), ), ], ), ); } // ── 保存按钮 ── Widget _buildSaveButton() { final l10n = AppLocalizations.of(context); return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, boxShadow: const [ BoxShadow( color: Color(0x15000000), blurRadius: 8, offset: Offset(0, -2)), ], ), child: GestureDetector( onTap: () { if (_isSelfAdmin && !(_checked['admin.permissions'] ?? false)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('无法取消自己的管理员权限'), duration: Duration(seconds: 2)), ); return; } widget.onSave(); }, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(22), ), child: Text( l10n.get('confirmSave'), textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ); } } class _ChangeLog { final String time; final String operator; final String summary; const _ChangeLog({ required this.time, required this.operator, required this.summary, }); }