二次开发源码提交

This commit is contained in:
2026-05-15 08:52:48 +08:00
parent 4131f7321a
commit eb35a1d067
191 changed files with 34566 additions and 0 deletions
@@ -0,0 +1,86 @@
import 'package:cloudreve4_flutter/presentation/providers/admin_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/auth_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/user_setting_provider.dart';
import 'package:cloudreve4_flutter/presentation/pages/profile/widgets/profile_info_card.dart';
import 'package:cloudreve4_flutter/presentation/pages/profile/widgets/quick_functions_section.dart';
import 'package:cloudreve4_flutter/presentation/pages/profile/widgets/admin_section.dart';
import 'package:cloudreve4_flutter/services/avatar_cache_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// "我的"页面
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_initData();
});
}
Future<void> _initData() async {
final userSetting = context.read<UserSettingProvider>();
userSetting.loadCapacity();
final authProvider = context.read<AuthProvider>();
if (!authProvider.isAdmin) return;
final adminProvider = context.read<AdminProvider>();
if (adminProvider.groups.isNotEmpty || adminProvider.users.isNotEmpty) return;
await adminProvider.loadAll();
if (!mounted) return;
// 批量检查头像更新,限制并发避免阻塞主线程
final users = adminProvider.users;
final baseUrl = authProvider.currentServer?.baseUrl ?? '';
final token = authProvider.token?.accessToken ?? '';
final needCheckIds = <String>[];
for (final user in users) {
final userId = user.hashId ?? user.id.toString();
if (AvatarCacheService.instance.avatarIsExist(userId)) {
needCheckIds.add(userId);
}
}
if (needCheckIds.isNotEmpty) {
AvatarCacheService.instance.batchCheckUpdates(
needCheckIds,
baseUrl: baseUrl,
token: token,
);
}
}
@override
Widget build(BuildContext context) {
final isAdmin = context.select<AuthProvider, bool>((p) => p.isAdmin);
return Scaffold(
appBar: AppBar(title: const Text('我的')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ProfileInfoCard(),
const SizedBox(height: 16),
const QuickFunctionsSection(),
if (isAdmin) ...[
const SizedBox(height: 16),
const AdminSection(),
],
const SizedBox(height: 16),
],
),
),
);
}
}
@@ -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),
);
}
@@ -0,0 +1,115 @@
import 'package:cloudreve4_flutter/presentation/providers/auth_provider.dart';
import 'package:cloudreve4_flutter/presentation/providers/user_setting_provider.dart';
import 'package:cloudreve4_flutter/presentation/pages/profile/widgets/thick_storage_bar.dart';
import 'package:cloudreve4_flutter/presentation/widgets/user_avatar.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// 顶部用户信息卡片
class ProfileInfoCard extends StatelessWidget {
const ProfileInfoCard({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final authProvider = context.watch<AuthProvider>();
final user = authProvider.user;
final displayName = user?.nickname ?? '用户';
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
UserAvatar(
userId: user?.id ?? '',
email: user?.email,
displayName: displayName,
radius: 32,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Row(
children: [
Text(
'ID: ${user?.id ?? '-'}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
),
),
if (authProvider.isAdmin) ...[
const SizedBox(width: 8),
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,
),
),
),
],
],
),
const SizedBox(height: 4),
if (user?.email != null)
Text(
user!.email!,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
),
),
const SizedBox(height: 4),
Text(
'注册于 ${_formatDate(user?.createdAt)}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Consumer<UserSettingProvider>(
builder: (context, userSetting, _) {
final capacity = userSetting.capacity;
return ThickStorageBar(
used: capacity?.used ?? 0,
total: capacity?.total ?? 0,
percentage: capacity?.usagePercentage ?? 0,
);
},
),
],
),
),
);
}
String _formatDate(DateTime? date) {
if (date == null) return '-';
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}
@@ -0,0 +1,138 @@
import 'package:cloudreve4_flutter/router/app_router.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class _QuickFunction {
final IconData icon;
final String label;
final String route;
const _QuickFunction({
required this.icon,
required this.label,
required this.route,
});
}
class QuickFunctionsSection extends StatelessWidget {
const QuickFunctionsSection({super.key});
static const _functions = [
_QuickFunction(icon: LucideIcons.share2, label: '我的分享', route: RouteNames.share),
_QuickFunction(icon: LucideIcons.cloud, label: 'WebDAV', route: RouteNames.webdav),
_QuickFunction(icon: LucideIcons.download, label: '离线下载', route: RouteNames.remoteDownload),
_QuickFunction(icon: LucideIcons.trash2, label: '回收站', route: RouteNames.recycleBin),
_QuickFunction(icon: LucideIcons.settings, label: '设置', route: RouteNames.settings),
];
static const double _spacing = 12;
static const double _runSpacing = 4;
static const double _minItemWidth = 120;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 14),
child: Row(
children: [
Icon(LucideIcons.zap, size: 18, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Text('快捷功能',
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w600)),
],
),
),
LayoutBuilder(
builder: (context, constraints) {
final availableWidth = constraints.maxWidth;
int perRow = 1;
while (perRow < _functions.length) {
final next = perRow + 1;
final itemWidth = (availableWidth - _spacing * (next - 1)) / next;
if (itemWidth < _minItemWidth) break;
perRow = next;
}
final itemWidth = (availableWidth - _spacing * (perRow - 1)) / perRow;
return Wrap(
spacing: _spacing,
runSpacing: _runSpacing,
children: _functions.map((fn) {
return SizedBox(
width: itemWidth,
child: _QuickFunctionCard(
icon: fn.icon,
label: fn.label,
onTap: () => Navigator.of(context).pushNamed(fn.route),
),
);
}).toList(),
);
},
),
],
);
}
}
class _QuickFunctionCard extends StatefulWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _QuickFunctionCard({
required this.icon,
required this.label,
required this.onTap,
});
@override
State<_QuickFunctionCard> createState() => _QuickFunctionCardState();
}
class _QuickFunctionCardState extends State<_QuickFunctionCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
color: _hovered
? colorScheme.surfaceContainerHighest
: null,
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(12),
onHover: (v) => setState(() => _hovered = v),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
child: Row(
children: [
Icon(widget.icon, size: 20, color: colorScheme.primary),
const SizedBox(width: 10),
Flexible(
child: Text(
widget.label,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: _hovered ? colorScheme.primary : null,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
/// 粗进度条存储指示器
class ThickStorageBar extends StatelessWidget {
final int used;
final int total;
final double percentage;
const ThickStorageBar({
super.key,
required this.used,
required this.total,
required this.percentage,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: total > 0 ? (used / total).clamp(0.0, 1.0) : 0,
minHeight: 10,
backgroundColor: colorScheme.primary.withValues(alpha: 0.12),
valueColor: AlwaysStoppedAnimation<Color>(colorScheme.primary),
borderRadius: BorderRadius.circular(6),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'已使用 ${_formatBytes(used)}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
),
),
Text(
'${_formatBytes(total)}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.hintColor,
),
),
],
),
],
);
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}