123 lines
4.0 KiB
Dart
123 lines
4.0 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
|
import 'package:go_router/go_router.dart';
|
|||
|
|
|
|||
|
|
import '../../providers/auth_provider.dart';
|
|||
|
|
import '../../utils/i10n.dart';
|
|||
|
|
import '../../utils/permissions.dart';
|
|||
|
|
import '../../utils/responsive.dart';
|
|||
|
|
|
|||
|
|
/// 管理中心页(底部导航第 4 个 tab):各管理模块入口。
|
|||
|
|
///
|
|||
|
|
/// 模块可见性由 [SessionPermissions] 统一驱动,与 [router.dart] 的路由可达性
|
|||
|
|
/// 保持一致:
|
|||
|
|
/// - 用户管理:所有已登录账号可见(数据范围由后端按会话权限收敛);
|
|||
|
|
/// - 渠道商管理:管理员 + 渠道商可见,代理商不可见;
|
|||
|
|
/// - 其余模块(设备/激活码/公司介绍/滚动字幕)对所有账号可见。
|
|||
|
|
class AdminHubPage extends ConsumerWidget {
|
|||
|
|
const AdminHubPage({super.key});
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|||
|
|
final t = AppLocalizations.of(context);
|
|||
|
|
// 注:按需求「设备分组」整合进设备管理功能(设备详情/设备列表内可改分组),
|
|||
|
|
// 「应用管理」移除以简化管理页。故网格不再单列这两项。
|
|||
|
|
final scheme = Theme.of(context).colorScheme;
|
|||
|
|
final user = ref.watch(authProvider).user;
|
|||
|
|
// 菜单可见性与路由可达性由同一套 SessionPermissions 谓词驱动:
|
|||
|
|
// - 用户管理:所有已登录账号(各角色看自己范围内用户);
|
|||
|
|
// - 渠道商管理:管理员 + 渠道商(代理商不可见,路由层亦会拦截其深链)。
|
|||
|
|
final showUserManagement = SessionPermissions.canViewUserManagement(user);
|
|||
|
|
final showChannelManagement =
|
|||
|
|
SessionPermissions.canViewChannelManagement(user);
|
|||
|
|
|
|||
|
|
final items = [
|
|||
|
|
_MenuItem(
|
|||
|
|
title: t.deviceManagement,
|
|||
|
|
icon: Icons.devices_outlined,
|
|||
|
|
color: scheme.primary,
|
|||
|
|
route: '/admin/device',
|
|||
|
|
),
|
|||
|
|
_MenuItem(
|
|||
|
|
title: t.activationCode,
|
|||
|
|
icon: Icons.vpn_key_outlined,
|
|||
|
|
color: Colors.amber,
|
|||
|
|
route: '/admin/activation',
|
|||
|
|
),
|
|||
|
|
if (showUserManagement)
|
|||
|
|
_MenuItem(
|
|||
|
|
title: t.userManagement,
|
|||
|
|
icon: Icons.people_outlined,
|
|||
|
|
color: scheme.outline,
|
|||
|
|
route: '/admin/sys/user',
|
|||
|
|
),
|
|||
|
|
_MenuItem(
|
|||
|
|
title: t.companyIntro,
|
|||
|
|
icon: Icons.business_outlined,
|
|||
|
|
color: Colors.teal,
|
|||
|
|
route: '/admin/intro/store-list',
|
|||
|
|
),
|
|||
|
|
_MenuItem(
|
|||
|
|
title: t.marqueeManagement,
|
|||
|
|
icon: Icons.subtitles_outlined,
|
|||
|
|
color: Colors.deepPurple,
|
|||
|
|
route: '/admin/marquee',
|
|||
|
|
),
|
|||
|
|
if (showChannelManagement)
|
|||
|
|
_MenuItem(
|
|||
|
|
title: t.channelManagement,
|
|||
|
|
icon: Icons.account_tree_outlined,
|
|||
|
|
color: Colors.blue,
|
|||
|
|
route: '/admin/channel',
|
|||
|
|
),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return Scaffold(
|
|||
|
|
appBar: AppBar(title: Text(t.admin)),
|
|||
|
|
body: GridView.builder(
|
|||
|
|
padding: const EdgeInsets.all(16),
|
|||
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|||
|
|
crossAxisCount: context.responsive.gridCount(minTileWidth: 300),
|
|||
|
|
mainAxisSpacing: 12,
|
|||
|
|
crossAxisSpacing: 12,
|
|||
|
|
childAspectRatio: 1.2,
|
|||
|
|
),
|
|||
|
|
itemCount: items.length,
|
|||
|
|
itemBuilder: (context, index) {
|
|||
|
|
final item = items[index];
|
|||
|
|
return Card(
|
|||
|
|
child: InkWell(
|
|||
|
|
onTap: () => context.push(item.route),
|
|||
|
|
child: Column(
|
|||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|||
|
|
children: [
|
|||
|
|
Icon(item.icon, size: 40, color: item.color),
|
|||
|
|
const SizedBox(height: 8),
|
|||
|
|
Text(
|
|||
|
|
item.title,
|
|||
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _MenuItem {
|
|||
|
|
const _MenuItem({
|
|||
|
|
required this.title,
|
|||
|
|
required this.icon,
|
|||
|
|
required this.color,
|
|||
|
|
required this.route,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
final String title;
|
|||
|
|
final IconData icon;
|
|||
|
|
final Color color;
|
|||
|
|
final String route;
|
|||
|
|
}
|