二次开发源码提交
This commit is contained in:
@@ -0,0 +1,838 @@
|
||||
import 'package:cloudreve4_flutter/data/models/admin_model.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/providers/admin_provider.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/toast_helper.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/user_avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// 管理员功能区域
|
||||
class AdminSection extends StatelessWidget {
|
||||
const AdminSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.shield, size: 18, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('管理',
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Selector<AdminProvider, bool>(
|
||||
selector: (_, p) => p.isLoading,
|
||||
builder: (_, isLoading, _) {
|
||||
if (isLoading) {
|
||||
return const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Selector<AdminProvider, (List<AdminGroupModel>, PaginationModel?)>(
|
||||
selector: (_, p) => (p.groups, p.groupsPagination),
|
||||
builder: (_, data, _) => _GroupsCard(groups: data.$1, pagination: data.$2),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Selector<AdminProvider, (List<AdminUserModel>, PaginationModel?)>(
|
||||
selector: (_, p) => (p.users, p.usersPagination),
|
||||
builder: (_, data, _) => _UsersCard(users: data.$1, pagination: data.$2),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户组卡片 ====================
|
||||
|
||||
class _GroupsCard extends StatelessWidget {
|
||||
final List<AdminGroupModel> groups;
|
||||
final PaginationModel? pagination;
|
||||
|
||||
const _GroupsCard({required this.groups, this.pagination});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.users, size: 18, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('用户组',
|
||||
style: theme.textTheme.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
if (pagination != null)
|
||||
Text(
|
||||
'共 ${pagination!.totalItems} 个',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.outlined(
|
||||
icon: const Icon(LucideIcons.plus, size: 18),
|
||||
onPressed: () => _showCreateGroupDialog(context),
|
||||
tooltip: '创建用户组',
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
padding: EdgeInsets.zero,
|
||||
style: IconButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (groups.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: Text('暂无数据',
|
||||
style: TextStyle(color: theme.hintColor)),
|
||||
),
|
||||
)
|
||||
else
|
||||
...groups.map((group) => _GroupItem(group: group)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateGroupDialog(BuildContext context) {
|
||||
final controller = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _StyledDialog(
|
||||
title: '创建用户组',
|
||||
icon: LucideIcons.users,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: _StyledInputDecoration(
|
||||
labelText: '用户组名称',
|
||||
hintText: '请输入用户组名称',
|
||||
),
|
||||
),
|
||||
onConfirm: () {
|
||||
final name = controller.text.trim();
|
||||
if (name.isEmpty) return false;
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<AdminProvider>().createGroup(name).then((success) {
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
ToastHelper.success('创建成功');
|
||||
} else {
|
||||
ToastHelper.failure('创建失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GroupItem extends StatelessWidget {
|
||||
final AdminGroupModel group;
|
||||
|
||||
const _GroupItem({required this.group});
|
||||
|
||||
bool get _isAdmin => group.name.toLowerCase() == 'admin' || group.name == '管理员';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
group.name[0],
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(group.name,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'ID: ${group.id} • ${group.formattedMaxStorage}',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_isAdmin)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text('管理员',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600)),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: Icon(LucideIcons.trash2, size: 16, color: colorScheme.error.withValues(alpha: 0.7)),
|
||||
onPressed: () => _confirmDeleteGroup(context, group),
|
||||
tooltip: '删除',
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDeleteGroup(BuildContext context, AdminGroupModel group) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _StyledConfirmDialog(
|
||||
title: '删除用户组',
|
||||
message: '确定要删除用户组「${group.name}」吗?',
|
||||
icon: LucideIcons.trash2,
|
||||
isDestructive: true,
|
||||
onConfirm: () {
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<AdminProvider>().deleteGroup(group.id).then((error) {
|
||||
if (context.mounted) {
|
||||
if (error != null) {
|
||||
ToastHelper.failure(error);
|
||||
} else {
|
||||
ToastHelper.success('已删除');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户卡片 ====================
|
||||
|
||||
class _UsersCard extends StatelessWidget {
|
||||
final List<AdminUserModel> users;
|
||||
final PaginationModel? pagination;
|
||||
|
||||
const _UsersCard({required this.users, this.pagination});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final adminProvider = context.read<AdminProvider>();
|
||||
|
||||
return Selector<AdminProvider, (bool, int)>(
|
||||
selector: (_, p) => (p.isSelectingUsers, p.selectedUserIds.length),
|
||||
builder: (_, data, _) {
|
||||
final isSelecting = data.$1;
|
||||
final selectedCount = data.$2;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.user, size: 18, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('用户',
|
||||
style: theme.textTheme.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
if (pagination != null)
|
||||
Text(
|
||||
'共 ${pagination!.totalItems} 个',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isSelecting) ...[
|
||||
if (selectedCount > 0)
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmBatchDelete(context, adminProvider.selectedUserIds.toList()),
|
||||
icon: Icon(LucideIcons.trash2, size: 16, color: colorScheme.error),
|
||||
label: Text('删除 ($selectedCount)',
|
||||
style: TextStyle(color: colorScheme.error)),
|
||||
style: TextButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
),
|
||||
IconButton.outlined(
|
||||
icon: const Icon(LucideIcons.x, size: 18),
|
||||
onPressed: () => adminProvider.exitSelectMode(),
|
||||
tooltip: '取消选择',
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
] else ...[
|
||||
IconButton.outlined(
|
||||
icon: const Icon(LucideIcons.checkSquare, size: 18),
|
||||
onPressed: () => adminProvider.toggleSelectMode(),
|
||||
tooltip: '多选',
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton.outlined(
|
||||
icon: const Icon(LucideIcons.plus, size: 18),
|
||||
onPressed: () => _showCreateUserDialog(context),
|
||||
tooltip: '创建用户',
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (isSelecting)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => adminProvider.selectAllUsers(),
|
||||
child: const Text('全选'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => adminProvider.clearUserSelection(),
|
||||
child: const Text('取消全选'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (users.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: Text('暂无数据',
|
||||
style: TextStyle(color: theme.hintColor)),
|
||||
),
|
||||
)
|
||||
else
|
||||
...users.map((user) => _UserItem(user: user)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateUserDialog(BuildContext context) {
|
||||
final emailController = TextEditingController();
|
||||
final nickController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final groups = context.read<AdminProvider>().groups;
|
||||
int? selectedGroupId;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) => _StyledDialog(
|
||||
title: '创建用户',
|
||||
icon: LucideIcons.userPlus,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: emailController,
|
||||
decoration: _StyledInputDecoration(
|
||||
labelText: '邮箱',
|
||||
hintText: 'user@example.com',
|
||||
prefixIcon: Icon(LucideIcons.mail, size: 18),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: nickController,
|
||||
decoration: _StyledInputDecoration(
|
||||
labelText: '昵称',
|
||||
hintText: '请输入昵称',
|
||||
prefixIcon: Icon(LucideIcons.user, size: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
decoration: _StyledInputDecoration(
|
||||
labelText: '密码',
|
||||
hintText: '请输入密码',
|
||||
prefixIcon: Icon(LucideIcons.lock, size: 18),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_GroupChipSelector(
|
||||
groups: groups,
|
||||
selectedGroupId: selectedGroupId,
|
||||
onChanged: (id) => setDialogState(() => selectedGroupId = id),
|
||||
),
|
||||
],
|
||||
),
|
||||
onConfirm: () {
|
||||
final email = emailController.text.trim();
|
||||
final nick = nickController.text.trim();
|
||||
final password = passwordController.text.trim();
|
||||
if (email.isEmpty || nick.isEmpty || password.isEmpty || selectedGroupId == null) {
|
||||
ToastHelper.error('请填写完整信息');
|
||||
return false;
|
||||
}
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<AdminProvider>().createUser(
|
||||
email: email,
|
||||
nick: nick,
|
||||
password: password,
|
||||
groupId: selectedGroupId!,
|
||||
).then((success) {
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
ToastHelper.success('创建成功');
|
||||
} else {
|
||||
ToastHelper.failure('创建失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmBatchDelete(BuildContext context, List<int> ids) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _StyledConfirmDialog(
|
||||
title: '批量删除用户',
|
||||
message: '确定要删除选中的 ${ids.length} 个用户吗?此操作不可撤销。',
|
||||
icon: LucideIcons.trash2,
|
||||
isDestructive: true,
|
||||
onConfirm: () {
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<AdminProvider>().batchDeleteUsers(ids).then((success) {
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
ToastHelper.success('已删除');
|
||||
} else {
|
||||
ToastHelper.failure('删除失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户组 Chip 选择器 — 替代 DropdownButtonFormField
|
||||
class _GroupChipSelector extends StatelessWidget {
|
||||
final List<AdminGroupModel> groups;
|
||||
final int? selectedGroupId;
|
||||
final ValueChanged<int?> onChanged;
|
||||
|
||||
const _GroupChipSelector({
|
||||
required this.groups,
|
||||
this.selectedGroupId,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('用户组',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: groups.map((group) {
|
||||
final selected = selectedGroupId == group.id;
|
||||
return ChoiceChip(
|
||||
label: Text(group.name),
|
||||
selected: selected,
|
||||
onSelected: (_) => onChanged(selected ? null : group.id),
|
||||
avatar: selected
|
||||
? null
|
||||
: CircleAvatar(
|
||||
radius: 10,
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
child: Text(
|
||||
group.name[0],
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (selectedGroupId == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
'请选择用户组',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.error.withValues(alpha: 0.7),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserItem extends StatelessWidget {
|
||||
final AdminUserModel user;
|
||||
|
||||
const _UserItem({required this.user});
|
||||
|
||||
bool _isAdminGroup(AdminGroupModel group) {
|
||||
final name = group.name.toLowerCase();
|
||||
return name == 'admin' || name == '管理员';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Selector<AdminProvider, (bool, bool)>(
|
||||
selector: (_, p) => (p.isSelectingUsers, p.isUserSelected(user.id)),
|
||||
builder: (_, data, _) {
|
||||
final isSelecting = data.$1;
|
||||
final isSelected = data.$2;
|
||||
final adminProvider = context.read<AdminProvider>();
|
||||
|
||||
return InkWell(
|
||||
onTap: isSelecting ? () => adminProvider.toggleUserSelection(user.id) : null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (isSelecting)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) => adminProvider.toggleUserSelection(user.id),
|
||||
),
|
||||
),
|
||||
UserAvatar(
|
||||
userId: user.hashId ?? user.id.toString(),
|
||||
email: user.email,
|
||||
displayName: user.nick,
|
||||
radius: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(user.nick,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (user.group != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: _isAdminGroup(user.group!)
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(user.group!.name,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: _isAdminGroup(user.group!)
|
||||
? colorScheme.onPrimaryContainer
|
||||
: theme.hintColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${user.email} • ${user.formattedStorage}',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.hintColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 统一风格对话框组件 ====================
|
||||
|
||||
/// 圆角风格对话框 — 与页面 Card 风格统一
|
||||
class _StyledDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Widget child;
|
||||
final bool Function() onConfirm;
|
||||
|
||||
const _StyledDialog({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.child,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 18, color: colorScheme.onPrimaryContainer),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
child,
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(
|
||||
onPressed: () => onConfirm(),
|
||||
style: FilledButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
),
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 圆角确认对话框
|
||||
class _StyledConfirmDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final bool isDestructive;
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
const _StyledConfirmDialog({
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.icon,
|
||||
this.isDestructive = false,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isDestructive
|
||||
? colorScheme.errorContainer
|
||||
: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon,
|
||||
size: 18,
|
||||
color: isDestructive
|
||||
? colorScheme.onErrorContainer
|
||||
: colorScheme.onPrimaryContainer),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(message, style: theme.textTheme.bodyMedium),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(
|
||||
onPressed: onConfirm,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: isDestructive ? colorScheme.error : null,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
),
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一输入框装饰 — 填充背景 + 圆角
|
||||
class _StyledInputDecoration extends InputDecoration {
|
||||
const _StyledInputDecoration({
|
||||
super.labelText,
|
||||
super.hintText,
|
||||
super.prefixIcon,
|
||||
}) : super(
|
||||
filled: true,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user