2026-03-12 10:23:21 +08:00
|
|
|
|
import 'package:flutter/foundation.dart';
|
2025-12-12 09:11:30 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
2026-03-12 10:23:21 +08:00
|
|
|
|
import 'package:qhd_prevention/common/route_model.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/customWidget/toast_util.dart';
|
2025-12-12 09:11:30 +08:00
|
|
|
|
import 'package:qhd_prevention/customWidget/work_tab_icon_grid.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/http/ApiService.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/customWidget/IconBadgeButton.dart';
|
2025-12-24 16:07:53 +08:00
|
|
|
|
import 'package:qhd_prevention/pages/home/unit/unit_join_list_page.dart';
|
2025-12-12 09:11:30 +08:00
|
|
|
|
import 'package:qhd_prevention/pages/my_appbar.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/tools/tools.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/common/route_aware_state.dart';
|
2026-03-12 10:23:21 +08:00
|
|
|
|
import 'package:qhd_prevention/common/route_service.dart';
|
2025-12-12 09:11:30 +08:00
|
|
|
|
|
|
|
|
|
|
class UnitTabPage extends StatefulWidget {
|
|
|
|
|
|
const UnitTabPage({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<UnitTabPage> createState() => _UnitTabPageState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _UnitTabPageState extends RouteAwareState<UnitTabPage> {
|
2026-03-12 10:23:21 +08:00
|
|
|
|
// 原始 master 定义(固定顺序,不随权限变动)
|
|
|
|
|
|
final List<Map<String, dynamic>> _masterButtons = [
|
2025-12-12 09:11:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
"icon": "assets/images/unit_ico1.png",
|
|
|
|
|
|
"title": "服务单位管理",
|
|
|
|
|
|
"unreadCount": 0,
|
2026-03-12 10:23:21 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-12-12 09:11:30 +08:00
|
|
|
|
"icon": "assets/images/unit_ico2.png",
|
|
|
|
|
|
"title": "就职单位管理",
|
|
|
|
|
|
"unreadCount": 0,
|
2026-03-12 10:23:21 +08:00
|
|
|
|
},
|
2025-12-12 09:11:30 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-12 10:23:21 +08:00
|
|
|
|
// title -> 后端 menuPerm 映射(基于你提供的路由配置)
|
|
|
|
|
|
final Map<String, String> _permMapping = {
|
|
|
|
|
|
"服务单位管理": "dashboard-Unit-Management-Managee-Service-Unit-Management",
|
|
|
|
|
|
"就职单位管理": "dashboard-Unit-Management-Employment-Unit",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 当前哪些按钮应显示(与 master 顺序对应)
|
|
|
|
|
|
late List<bool> _visible;
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
2026-03-12 10:23:21 +08:00
|
|
|
|
// 初始:全部显示,避免短暂白屏(路由加载后会更新)
|
|
|
|
|
|
_visible = List<bool>.filled(_masterButtons.length, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 监听路由变化,路由加载完成或变更时刷新可见性
|
|
|
|
|
|
RouteService().addListener(_onRouteUpdated);
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试一次应用路由(如果已经加载)
|
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateVisibilityFromRoute());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
RouteService().removeListener(_onRouteUpdated);
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _onRouteUpdated() {
|
|
|
|
|
|
// 当路由配置更新时重计算按钮可见性
|
|
|
|
|
|
_updateVisibilityFromRoute();
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
|
|
|
|
|
|
void _updateVisibilityFromRoute() {
|
|
|
|
|
|
final routeService = RouteService();
|
|
|
|
|
|
|
|
|
|
|
|
// routesLoaded: 当 mainTabs 非空时认为路由加载完毕(开始严格按后端配置显示)
|
|
|
|
|
|
final bool routesLoaded = routeService.mainTabs.isNotEmpty;
|
|
|
|
|
|
|
|
|
|
|
|
final List<bool> next = List<bool>.filled(_masterButtons.length, false);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < _masterButtons.length; i++) {
|
|
|
|
|
|
final title = _masterButtons[i]['title'] as String;
|
|
|
|
|
|
final perm = _permMapping[title] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
if (!routesLoaded) {
|
|
|
|
|
|
// 路由还没加载:保持默认显示(避免闪烁)
|
|
|
|
|
|
next[i] = true;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (perm.isEmpty) {
|
|
|
|
|
|
// 没有映射:默认隐藏
|
|
|
|
|
|
next[i] = false;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 优先使用 RouteService.findRouteByPerm 获取路由节点(该方法会考虑节点 visible)
|
|
|
|
|
|
final RouteModel? node = routeService.findRouteByPerm(perm);
|
|
|
|
|
|
|
|
|
|
|
|
if (node != null) {
|
|
|
|
|
|
// 按你的要求:以 showFlag == 1 判断是否显示(兼容旧字段)
|
|
|
|
|
|
next[i] = (node.showFlag == 1) || (node.visible);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有找到完全匹配的 menuPerm,有可能后端使用不同层级或不同 perms 命名,
|
|
|
|
|
|
// 这里做一次容错:在所有顶级下递归查找 menuPerm 相等项(不依赖 findRouteByPerm)。
|
|
|
|
|
|
RouteModel? fallback;
|
|
|
|
|
|
for (final top in routeService.allRoutes) {
|
|
|
|
|
|
fallback = _findRouteRecursiveByPerm(top, perm);
|
|
|
|
|
|
if (fallback != null) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
next[i] = fallback != null ? ((fallback.showFlag == 1) || (fallback.visible)) : false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只有在发生变化时才 setState
|
|
|
|
|
|
if (!listEquals(next, _visible)) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_visible = next;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 递归:在以 node 为根的子树中查找第一个 menuPerms 等于 targetPerm 的节点
|
|
|
|
|
|
/// (不依赖 RouteService.findRouteByPerm,作为 fallback)
|
|
|
|
|
|
RouteModel? _findRouteRecursiveByPerm(RouteModel node, String targetPerm) {
|
|
|
|
|
|
if ((node.menuPerms ?? '') == targetPerm) return node;
|
|
|
|
|
|
for (final c in node.children) {
|
|
|
|
|
|
final res = _findRouteRecursiveByPerm(c, targetPerm);
|
|
|
|
|
|
if (res != null) return res;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 当页面可见时拉取数据
|
|
|
|
|
|
@override
|
2025-12-12 09:11:30 +08:00
|
|
|
|
Future<void> onVisible() async {
|
2026-03-12 10:23:21 +08:00
|
|
|
|
await _getData();
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:23:21 +08:00
|
|
|
|
Future<void> _getData() async {
|
|
|
|
|
|
// TODO: 如果需要从后端拉取角标/统计,在这里实现并 setState 更新 _masterButtons[*]['unreadCount']
|
|
|
|
|
|
// await Future.delayed(Duration(milliseconds: 100));
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _handleIconTap(int index) async {
|
2026-03-12 10:23:21 +08:00
|
|
|
|
final title = _masterButtons[index]['title'] as String;
|
|
|
|
|
|
switch (title) {
|
|
|
|
|
|
case '服务单位管理':
|
|
|
|
|
|
ToastUtil.showNormal(context, '您还没有参与项目');
|
2025-12-12 09:11:30 +08:00
|
|
|
|
break;
|
2026-03-12 10:23:21 +08:00
|
|
|
|
case '就职单位管理':
|
2025-12-24 16:07:53 +08:00
|
|
|
|
pushPage(UnitJoinListPage(), context);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 点击后可以刷新数据
|
|
|
|
|
|
await _getData();
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-03-12 10:23:21 +08:00
|
|
|
|
final double screenW = MediaQuery.of(context).size.width;
|
|
|
|
|
|
final double bannerHeight = 618 / 1125 * screenW;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据可见按钮数量决定 icon 区高度(每行最多 4 个)
|
|
|
|
|
|
final visibleButtons = <Map<String, dynamic>>[];
|
|
|
|
|
|
for (int i = 0; i < _masterButtons.length; i++) {
|
|
|
|
|
|
if (i < _visible.length && _visible[i]) visibleButtons.add(_masterButtons[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
final int visibleCount = visibleButtons.length;
|
|
|
|
|
|
final int perRow = 4;
|
|
|
|
|
|
final int rows = visibleCount == 0 ? 0 : ((visibleCount + perRow - 1) ~/ perRow);
|
|
|
|
|
|
|
|
|
|
|
|
// 样式参数(如需微调)
|
|
|
|
|
|
const double verticalPadding = 30.0;
|
|
|
|
|
|
const double perRowHeight = 110.0; // 单行高度(图标 + 文本 + 内间距)
|
|
|
|
|
|
const double rowSpacing = 20.0;
|
|
|
|
|
|
final double iconSectionHeight = visibleCount == 0 ? 150.0 : (verticalPadding + rows * perRowHeight + (rows - 1) * rowSpacing);
|
|
|
|
|
|
|
|
|
|
|
|
const double iconOverlapBanner = 30.0;
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
return PopScope(
|
2026-02-28 14:38:07 +08:00
|
|
|
|
canPop: true,
|
2025-12-12 09:11:30 +08:00
|
|
|
|
child: Scaffold(
|
|
|
|
|
|
extendBodyBehindAppBar: true,
|
|
|
|
|
|
appBar: MyAppbar(
|
|
|
|
|
|
title: '单位管理',
|
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
|
),
|
|
|
|
|
|
body: ListView(
|
|
|
|
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
height: bannerHeight + iconSectionHeight,
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
clipBehavior: Clip.none,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
height: bannerHeight,
|
|
|
|
|
|
child: _buildBannerSection(bannerHeight),
|
|
|
|
|
|
),
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
left: 10,
|
|
|
|
|
|
right: 10,
|
|
|
|
|
|
top: bannerHeight - iconOverlapBanner,
|
|
|
|
|
|
height: iconSectionHeight,
|
2026-03-12 10:23:21 +08:00
|
|
|
|
child: _buildIconSection(context, visibleButtons, rows),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
|
|
|
|
|
|
// Banner
|
2025-12-12 09:11:30 +08:00
|
|
|
|
Widget _buildBannerSection(double height) {
|
|
|
|
|
|
return Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Image.asset(
|
|
|
|
|
|
"assets/images/unit_banner.jpg",
|
|
|
|
|
|
width: MediaQuery.of(context).size.width,
|
|
|
|
|
|
height: height,
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:23:21 +08:00
|
|
|
|
// 根据 visibleButtons 与行数构建 icon 区
|
|
|
|
|
|
Widget _buildIconSection(BuildContext context, List<Map<String, dynamic>> visibleButtons, int rows) {
|
|
|
|
|
|
if (visibleButtons.isEmpty) {
|
|
|
|
|
|
if (RouteService().mainTabs.isNotEmpty) {
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Center(child: Text('暂无权限访问的功能')),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
);
|
|
|
|
|
|
} else {
|
2026-03-12 10:23:21 +08:00
|
|
|
|
return Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: const Center(child: SizedBox()),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建每一行(每行最多 4 个)
|
|
|
|
|
|
final List<Widget> rowsWidgets = [];
|
|
|
|
|
|
final int perRow = 4;
|
|
|
|
|
|
for (int r = 0; r < rows; r++) {
|
|
|
|
|
|
final start = r * perRow;
|
|
|
|
|
|
final end = (start + perRow) > visibleButtons.length ? visibleButtons.length : (start + perRow);
|
|
|
|
|
|
final rowItems = visibleButtons.sublist(start, end);
|
|
|
|
|
|
|
|
|
|
|
|
rowsWidgets.add(
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: List.generate(perRow, (i) {
|
|
|
|
|
|
final idx = start + i;
|
|
|
|
|
|
if (idx < visibleButtons.length) {
|
|
|
|
|
|
final btn = visibleButtons[idx];
|
|
|
|
|
|
// 找到在 master 中的真实索引(用于 onTap)
|
|
|
|
|
|
final masterIndex = _masterButtons.indexWhere((m) => m['title'] == btn['title']);
|
|
|
|
|
|
return Expanded(
|
|
|
|
|
|
child: Center(child: _buildIconButton(btn, masterIndex)),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return const Expanded(child: SizedBox.shrink());
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (r != rows - 1) rowsWidgets.add(const SizedBox(height: 20));
|
|
|
|
|
|
}
|
2025-12-12 09:11:30 +08:00
|
|
|
|
|
2026-03-12 10:23:21 +08:00
|
|
|
|
return Container(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5),
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(children: rowsWidgets),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
|
|
|
|
|
|
Widget _buildIconButton(Map<String, dynamic> info, int masterIndex) {
|
|
|
|
|
|
final unread = (info['unreadCount'] ?? 0) is int ? info['unreadCount'] as int : int.tryParse('${info['unreadCount']}') ?? 0;
|
2025-12-12 09:11:30 +08:00
|
|
|
|
return IconBadgeButton(
|
|
|
|
|
|
iconPath: info['icon'] ?? '',
|
|
|
|
|
|
title: info['title'] ?? '',
|
2026-03-12 10:23:21 +08:00
|
|
|
|
unreadCount: unread,
|
|
|
|
|
|
onTap: () => _handleIconTap(masterIndex),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-12 10:23:21 +08:00
|
|
|
|
}
|